diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION index e409227560d..37fe4887872 100644 --- a/GITALY_SERVER_VERSION +++ b/GITALY_SERVER_VERSION @@ -1 +1 @@ -e0e146ec58d2515c2767f77d2860afc4bb7c1c73 +e023bdab6cea4ad7124d64b12e77f2cdb0681e21 diff --git a/app/assets/javascripts/integrations/constants.js b/app/assets/javascripts/integrations/constants.js index b26f6bdd370..004601bc0a3 100644 --- a/app/assets/javascripts/integrations/constants.js +++ b/app/assets/javascripts/integrations/constants.js @@ -24,5 +24,3 @@ export const I18N_SUCCESSFUL_CONNECTION_MESSAGE = s__('Integrations|Connection s export const settingsTabTitle = __('Settings'); export const overridesTabTitle = s__('Integrations|Projects using custom settings'); - -export const INTEGRATION_FORM_SELECTOR = '.js-integration-settings-form'; diff --git a/app/assets/javascripts/integrations/edit/components/integration_form.vue b/app/assets/javascripts/integrations/edit/components/integration_form.vue index dcdb38d94c8..007a384f41e 100644 --- a/app/assets/javascripts/integrations/edit/components/integration_form.vue +++ b/app/assets/javascripts/integrations/edit/components/integration_form.vue @@ -8,7 +8,6 @@ import { I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE, I18N_DEFAULT_ERROR_MESSAGE, I18N_SUCCESSFUL_CONNECTION_MESSAGE, - INTEGRATION_FORM_SELECTOR, integrationLevels, } from '~/integrations/constants'; import { refreshCurrentPage } from '~/lib/utils/url_utility'; @@ -82,28 +81,9 @@ export default { disableButtons() { return Boolean(this.isSaving || this.isResetting || this.isTesting); }, - useVueForm() { - return this.glFeatures?.vueIntegrationForm; + form() { + return this.$refs.integrationForm.$el; }, - formContainerProps() { - return this.useVueForm - ? { - ref: 'integrationForm', - method: 'post', - class: 'gl-mb-3 gl-show-field-errors integration-settings-form', - action: this.propsSource.formPath, - novalidate: !this.integrationActive, - } - : {}; - }, - formContainer() { - return this.useVueForm ? GlForm : 'div'; - }, - }, - mounted() { - this.form = this.useVueForm - ? this.$refs.integrationForm.$el - : document.querySelector(INTEGRATION_FORM_SELECTOR); }, methods: { ...mapActions(['setOverride', 'requestJiraIssueTypes']), @@ -122,13 +102,13 @@ export default { this.form.submit(); }, onTestClick() { - this.isTesting = true; - if (!this.form.checkValidity()) { this.setIsValidated(); return; } + this.isTesting = true; + testIntegrationSettings(this.propsSource.testPath, this.getFormData()) .then(({ data: { error, message = I18N_FETCH_TEST_SETTINGS_DEFAULT_ERROR_MESSAGE } }) => { if (error) { @@ -171,16 +151,6 @@ export default { }, onToggleIntegrationState(integrationActive) { this.integrationActive = integrationActive; - if (!this.form || this.useVueForm) { - return; - } - - // If integration will be active, enable form validation. - if (integrationActive) { - this.form.removeAttribute('novalidate'); - } else { - this.form.setAttribute('novalidate', true); - } }, }, helpHtmlConfig: { @@ -193,17 +163,21 @@ export default { diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue index 3250a4818c7..7bad10616cc 100644 --- a/app/assets/javascripts/notes/components/noteable_note.vue +++ b/app/assets/javascripts/notes/components/noteable_note.vue @@ -3,6 +3,7 @@ import { GlSprintf, GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import $ from 'jquery'; import { escape, isEmpty } from 'lodash'; import { mapGetters, mapActions } from 'vuex'; +import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal'; import { INLINE_DIFF_LINES_KEY } from '~/diffs/constants'; import createFlash from '~/flash'; import httpStatusCodes from '~/lib/utils/http_status'; @@ -243,14 +244,18 @@ export default { this.setSelectedCommentPositionHover(); this.$emit('handleEdit'); }, - deleteHandler() { + async deleteHandler() { const typeOfComment = this.note.isDraft ? __('pending comment') : __('comment'); - if ( - // eslint-disable-next-line no-alert - window.confirm( - sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), { typeOfComment }), - ) - ) { + + const msg = sprintf(__('Are you sure you want to delete this %{typeOfComment}?'), { + typeOfComment, + }); + const confirmed = await confirmAction(msg, { + primaryBtnVariant: 'danger', + primaryBtnText: __('Delete Comment'), + }); + + if (confirmed) { this.isDeleting = true; this.$emit('handleDeleteNote', this.note); @@ -345,10 +350,11 @@ export default { parent: this.$el, }); }, - formCancelHandler({ shouldConfirm, isDirty }) { + async formCancelHandler({ shouldConfirm, isDirty }) { if (shouldConfirm && isDirty) { - // eslint-disable-next-line no-alert - if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) return; + const msg = __('Are you sure you want to cancel editing this comment?'); + const confirmed = await confirmAction(msg); + if (!confirmed) return; } this.$refs.noteBody.resetAutoSave(); if (this.oldContent) { diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js index 810d9f782b9..3d48c74b40b 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/constants.js @@ -23,9 +23,19 @@ export const DEFAULT_LABEL_NONE = { value: FILTER_NONE, text: __('None'), title: export const DEFAULT_LABEL_ANY = { value: FILTER_ANY, text: __('Any'), title: __('Any') }; export const DEFAULT_NONE_ANY = [DEFAULT_LABEL_NONE, DEFAULT_LABEL_ANY]; +export const DEFAULT_MILESTONE_UPCOMING = { + value: FILTER_UPCOMING, + text: __('Upcoming'), + title: __('Upcoming'), +}; +export const DEFAULT_MILESTONE_STARTED = { + value: FILTER_STARTED, + text: __('Started'), + title: __('Started'), +}; export const DEFAULT_MILESTONES = DEFAULT_NONE_ANY.concat([ - { value: FILTER_UPCOMING, text: __('Upcoming'), title: __('Upcoming') }, - { value: FILTER_STARTED, text: __('Started'), title: __('Started') }, + DEFAULT_MILESTONE_UPCOMING, + DEFAULT_MILESTONE_STARTED, ]); export const SortDirection = { diff --git a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue index bbc1888bc0b..157068b2c0f 100644 --- a/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue +++ b/app/assets/javascripts/vue_shared/components/filtered_search_bar/tokens/base_token.vue @@ -163,19 +163,22 @@ export default { }, }, methods: { - handleInput: debounce(function debouncedSearch({ data }) { - this.searchKey = data; + handleInput: debounce(function debouncedSearch({ data, operator }) { + // Prevent fetching suggestions when data or operator is not present + if (data || operator) { + this.searchKey = data; - if (!this.suggestionsLoading && !this.activeTokenValue) { - let search = this.searchTerm ? this.searchTerm : data; + if (!this.suggestionsLoading && !this.activeTokenValue) { + let search = this.searchTerm ? this.searchTerm : data; - if (search.startsWith('"') && search.endsWith('"')) { - search = stripQuotes(search); - } else if (search.startsWith('"')) { - search = search.slice(1, search.length); + if (search.startsWith('"') && search.endsWith('"')) { + search = stripQuotes(search); + } else if (search.startsWith('"')) { + search = search.slice(1, search.length); + } + + this.$emit('fetch-suggestions', search); } - - this.$emit('fetch-suggestions', search); } }, DEBOUNCE_DELAY), handleTokenValueSelected(selectedValue) { diff --git a/app/controllers/concerns/integrations/actions.rb b/app/controllers/concerns/integrations/actions.rb index f6e98c25b72..1f788860c8f 100644 --- a/app/controllers/concerns/integrations/actions.rb +++ b/app/controllers/concerns/integrations/actions.rb @@ -8,9 +8,6 @@ module Integrations::Actions include IntegrationsHelper before_action :integration, only: [:edit, :update, :overrides, :test] - before_action do - push_frontend_feature_flag(:vue_integration_form, current_user, default_enabled: :yaml) - end urgency :low, [:test] end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 4fabbe6a2b7..0079c56558a 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -321,7 +321,7 @@ class Projects::IssuesController < Projects::ApplicationController end def reorder_params - params.permit(:move_before_id, :move_after_id, :group_full_path) + params.permit(:move_before_id, :move_after_id) end def store_uri diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb index 9896f75c099..1321111faaf 100644 --- a/app/controllers/projects/services_controller.rb +++ b/app/controllers/projects/services_controller.rb @@ -12,9 +12,6 @@ class Projects::ServicesController < Projects::ApplicationController before_action :web_hook_logs, only: [:edit, :update] before_action :set_deprecation_notice_for_prometheus_integration, only: [:edit, :update] before_action :redirect_deprecated_prometheus_integration, only: [:update] - before_action do - push_frontend_feature_flag(:vue_integration_form, current_user, default_enabled: :yaml) - end respond_to :html diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb index 230f80e20a5..f5ba978e860 100644 --- a/app/helpers/integrations_helper.rb +++ b/app/helpers/integrations_helper.rb @@ -228,10 +228,6 @@ module IntegrationsHelper name: integration.to_param } end - - def vue_integration_form_enabled? - Feature.enabled?(:vue_integration_form, current_user, default_enabled: :yaml) - end end IntegrationsHelper.prepend_mod_with('IntegrationsHelper') diff --git a/app/models/namespaces/user_namespace.rb b/app/models/namespaces/user_namespace.rb index 14b867b2607..408acb6dcce 100644 --- a/app/models/namespaces/user_namespace.rb +++ b/app/models/namespaces/user_namespace.rb @@ -25,5 +25,9 @@ module Namespaces def self.sti_name 'User' end + + def owners + Array.wrap(owner) + end end end diff --git a/app/models/project.rb b/app/models/project.rb index 1b920ab57b0..abe072a3a6d 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1520,8 +1520,17 @@ class Project < ApplicationRecord group || namespace.try(:owner) end + def deprecated_owner + # Kept in order to maintain webhook structures until we remove owner_name and owner_email + # See https://gitlab.com/gitlab-org/gitlab/-/issues/350603 + group || namespace.try(:owner) + end + def owners - Array.wrap(owner) + # This will be phased out and replaced with `owners` relationship + # backed by memberships with direct/inherited Owner access roles + # See https://gitlab.com/groups/gitlab-org/-/epics/7405 + team.owners end def first_owner diff --git a/app/services/boards/base_item_move_service.rb b/app/services/boards/base_item_move_service.rb index be16c595abb..8bae810e676 100644 --- a/app/services/boards/base_item_move_service.rb +++ b/app/services/boards/base_item_move_service.rb @@ -39,7 +39,7 @@ module Boards end def reposition_params(reposition_ids) - reposition_parent.merge(move_between_ids: reposition_ids) + { move_between_ids: reposition_ids } end def move_single_issuable(issuable, issuable_modification_params) diff --git a/app/services/boards/issues/move_service.rb b/app/services/boards/issues/move_service.rb index 959a7fa3ad2..90226b9d4e0 100644 --- a/app/services/boards/issues/move_service.rb +++ b/app/services/boards/issues/move_service.rb @@ -54,10 +54,6 @@ module Boards def update(issue, issue_modification_params) ::Issues::UpdateService.new(project: issue.project, current_user: current_user, params: issue_modification_params).execute(issue) end - - def reposition_parent - { board_group_id: board.group&.id } - end end end end diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb index 6a8eec85d0c..11c6cc360de 100644 --- a/app/services/issuable_base_service.rb +++ b/app/services/issuable_base_service.rb @@ -495,10 +495,11 @@ class IssuableBaseService < ::BaseProjectService return unless params[:move_between_ids] after_id, before_id = params.delete(:move_between_ids) - positioning_scope_id = params.delete(positioning_scope_key) - issuable_before = issuable_for_positioning(before_id, positioning_scope_id) - issuable_after = issuable_for_positioning(after_id, positioning_scope_id) + positioning_scope = issuable_position.class.relative_positioning_query_base(issuable_position) + + issuable_before = issuable_for_positioning(before_id, positioning_scope) + issuable_after = issuable_for_positioning(after_id, positioning_scope) raise ActiveRecord::RecordNotFound unless issuable_before || issuable_after diff --git a/app/services/issues/reorder_service.rb b/app/services/issues/reorder_service.rb index 9c5fbec7d8e..8be4f530dac 100644 --- a/app/services/issues/reorder_service.rb +++ b/app/services/issues/reorder_service.rb @@ -2,47 +2,31 @@ module Issues class ReorderService < Issues::BaseService + include Gitlab::Utils::StrongMemoize + def execute(issue) return false unless can?(current_user, :update_issue, issue) - return false if group && !can?(current_user, :read_group, group) + return false unless move_between_ids - attrs = issue_params(group) - return false if attrs.empty? - - update(issue, attrs) + update(issue, { move_between_ids: move_between_ids }) end private - def group - return unless params[:group_full_path] - - @group ||= Group.find_by_full_path(params[:group_full_path]) - end - def update(issue, attrs) ::Issues::UpdateService.new(project: project, current_user: current_user, params: attrs).execute(issue) rescue ActiveRecord::RecordNotFound false end - def issue_params(group) - attrs = {} - - if move_between_ids - attrs[:move_between_ids] = move_between_ids - attrs[:board_group_id] = group&.id - end - - attrs - end - def move_between_ids - ids = [params[:move_after_id], params[:move_before_id]] - .map(&:to_i) - .map { |m| m > 0 ? m : nil } + strong_memoize(:move_between_ids) do + ids = [params[:move_after_id], params[:move_before_id]] + .map(&:to_i) + .map { |m| m > 0 ? m : nil } - ids.any? ? ids : nil + ids.any? ? ids : nil + end end end end diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb index c024cef2f82..4dc6294490f 100644 --- a/app/services/issues/update_service.rb +++ b/app/services/issues/update_service.rb @@ -100,10 +100,6 @@ module Issues rebalance_if_needed(issue) end - def positioning_scope_key - :board_group_id - end - # rubocop: disable CodeReuse/ActiveRecord def change_issue_duplicate(issue) canonical_issue_id = params.delete(:canonical_issue_id) @@ -221,20 +217,13 @@ module Issues ).execute end - # rubocop: disable CodeReuse/ActiveRecord - def issuable_for_positioning(id, board_group_id = nil) + def issuable_for_positioning(id, positioning_scope) return unless id - issue = - if board_group_id - IssuesFinder.new(current_user, group_id: board_group_id, include_subgroups: true).find_by(id: id) - else - project.issues.find(id) - end + issue = positioning_scope.find(id) issue if can?(current_user, :update_issue, issue) end - # rubocop: enable CodeReuse/ActiveRecord def create_confidentiality_note(issue) SystemNoteService.change_issue_confidentiality(issue, issue.project, current_user) diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml index 419dd827e49..65b93dc930a 100644 --- a/app/views/projects/services/_form.html.haml +++ b/app/views/projects/services/_form.html.haml @@ -6,13 +6,7 @@ - if integration.operating? = sprite_icon('check', css_class: 'gl-text-green-500') -- if vue_integration_form_enabled? - = render 'shared/integration_settings', integration: integration -- else - = form_for(integration, as: :service, url: scoped_integration_path(integration, project: @project, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => test_project_integration_path(@project, integration), testid: 'integration-form' } }) do |form| - = render 'shared/integration_settings', form: form, integration: integration - %input{ id: 'services_redirect_to', type: 'hidden', name: 'redirect_to', value: request.referer } - += render 'shared/integration_settings', integration: integration - if lookup_context.template_exists?('show', "projects/services/#{integration.to_param}", true) %hr = render "projects/services/#{integration.to_param}/show", integration: integration diff --git a/app/views/shared/integrations/_form.html.haml b/app/views/shared/integrations/_form.html.haml deleted file mode 100644 index e2457bc0632..00000000000 --- a/app/views/shared/integrations/_form.html.haml +++ /dev/null @@ -1,4 +0,0 @@ -- integration = local_assigns.fetch(:integration) - -= form_for integration, as: :service, url: scoped_integration_path(integration, group: @group), method: :put, html: { class: 'gl-show-field-errors integration-settings-form js-integration-settings-form', data: { 'test-url' => scoped_test_integration_path(integration, group: @group), testid: 'integration-form' } } do |form| - = render 'shared/integration_settings', form: form, integration: integration diff --git a/app/views/shared/integrations/edit.html.haml b/app/views/shared/integrations/edit.html.haml index 4ceaedc2a69..f2a31400698 100644 --- a/app/views/shared/integrations/edit.html.haml +++ b/app/views/shared/integrations/edit.html.haml @@ -7,7 +7,4 @@ = @integration.title = render 'shared/integrations/tabs', integration: @integration, active_tab: 'edit' do - - if vue_integration_form_enabled? - = render 'shared/integration_settings', integration: @integration - - else - = render 'shared/integrations/form', integration: @integration + = render 'shared/integration_settings', integration: @integration diff --git a/config/feature_flags/development/vue_integration_form.yml b/config/feature_flags/development/vue_integration_form.yml deleted file mode 100644 index a11c42b8d4a..00000000000 --- a/config/feature_flags/development/vue_integration_form.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: vue_integration_form -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/77934 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/350444 -milestone: '14.7' -type: development -group: group::integrations -default_enabled: false diff --git a/db/migrate/20220131135725_add_severity_level_to_merge_requests_compliance_violations.rb b/db/migrate/20220131135725_add_severity_level_to_merge_requests_compliance_violations.rb new file mode 100644 index 00000000000..50aa0121055 --- /dev/null +++ b/db/migrate/20220131135725_add_severity_level_to_merge_requests_compliance_violations.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddSeverityLevelToMergeRequestsComplianceViolations < Gitlab::Database::Migration[1.0] + def change + add_column :merge_requests_compliance_violations, :severity_level, :integer, limit: 2, null: false, default: 0 + end +end diff --git a/db/schema_migrations/20220131135725 b/db/schema_migrations/20220131135725 new file mode 100644 index 00000000000..d63b24d5d6c --- /dev/null +++ b/db/schema_migrations/20220131135725 @@ -0,0 +1 @@ +a0e9bb92512b9ddb3fd718e086b0fd116ec307be6acc5789a872b1d3004fdebd \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index f76f7cfd3d2..b1f0a258634 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -16489,7 +16489,8 @@ CREATE TABLE merge_requests_compliance_violations ( id bigint NOT NULL, violating_user_id bigint NOT NULL, merge_request_id bigint NOT NULL, - reason smallint NOT NULL + reason smallint NOT NULL, + severity_level smallint DEFAULT 0 NOT NULL ); CREATE SEQUENCE merge_requests_compliance_violations_id_seq diff --git a/doc/ci/troubleshooting.md b/doc/ci/troubleshooting.md index c72e5a31926..62913c56853 100644 --- a/doc/ci/troubleshooting.md +++ b/doc/ci/troubleshooting.md @@ -192,6 +192,21 @@ To illustrate its life cycle: The merge request pipeline widget shows information about the pipeline status in a merge request. It's displayed above the [ability to merge status widget](#merge-request-status-messages). +#### "Checking ability to merge automatically" message + +There is a [known issue](https://gitlab.com/gitlab-org/gitlab/-/issues/229352) +where a merge request can be stuck with the `Checking ability to merge automatically` +message. + +If your merge request has this message and it does not disappear after a few minutes, +you can try one of these workarounds: + +- Refresh the merge request page. +- Close & Re-open the merge request. +- Rebase the merge request with the `/rebase` [quick action](../user/project/quick_actions.md). +- If you have already confirmed the merge request is ready to be merged, you can merge + it with the `/merge` quick action. + #### "Checking pipeline status" message This message is shown when the merge request has no pipeline associated with the diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md index 44429754747..71d7e7f1426 100644 --- a/doc/system_hooks/system_hooks.md +++ b/doc/system_hooks/system_hooks.md @@ -79,8 +79,12 @@ X-Gitlab-Event: System Hook "updated_at": "2012-07-21T07:38:22Z", "event_name": "project_create", "name": "StoreCloud", - "owner_email": "johnsmith@gmail.com", + "owner_email": "johnsmith@example.com", "owner_name": "John Smith", + "owners": [{ + "name": "John", + "email": "user1@example.com" + }], "path": "storecloud", "path_with_namespace": "jsmith/storecloud", "project_id": 74, @@ -96,8 +100,12 @@ X-Gitlab-Event: System Hook "updated_at": "2012-07-21T07:38:22Z", "event_name": "project_destroy", "name": "Underscore", - "owner_email": "johnsmith@gmail.com", + "owner_email": "johnsmith@example.com", "owner_name": "John Smith", + "owners": [{ + "name": "John", + "email": "user1@example.com" + }], "path": "underscore", "path_with_namespace": "jsmith/underscore", "project_id": 73, @@ -117,7 +125,11 @@ X-Gitlab-Event: System Hook "path_with_namespace": "jsmith/underscore", "project_id": 73, "owner_name": "John Smith", - "owner_email": "johnsmith@gmail.com", + "owner_email": "johnsmith@example.com", + "owners": [{ + "name": "John", + "email": "user1@example.com" + }], "project_visibility": "internal", "old_path_with_namespace": "jsmith/overscore" } @@ -138,7 +150,11 @@ Please refer to `group_rename` and `user_rename` for that case. "path_with_namespace": "scores/underscore", "project_id": 73, "owner_name": "John Smith", - "owner_email": "johnsmith@gmail.com", + "owner_email": "johnsmith@example.com", + "owners": [{ + "name": "John", + "email": "user1@example.com" + }], "project_visibility": "internal", "old_path_with_namespace": "jsmith/overscore" } @@ -152,8 +168,12 @@ Please refer to `group_rename` and `user_rename` for that case. "updated_at": "2012-07-21T07:38:22Z", "event_name": "project_update", "name": "StoreCloud", - "owner_email": "johnsmith@gmail.com", + "owner_email": "johnsmith@example.com", "owner_name": "John Smith", + "owners": [{ + "name": "John", + "email": "user1@example.com" + }], "path": "storecloud", "path_with_namespace": "jsmith/storecloud", "project_id": 74, @@ -173,7 +193,7 @@ Please refer to `group_rename` and `user_rename` for that case. "project_name": "StoreCloud", "project_path": "storecloud", "project_path_with_namespace": "jsmith/storecloud", - "user_email": "johnsmith@gmail.com", + "user_email": "johnsmith@example.com", "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41, @@ -193,7 +213,7 @@ Please refer to `group_rename` and `user_rename` for that case. "project_name": "StoreCloud", "project_path": "storecloud", "project_path_with_namespace": "jsmith/storecloud", - "user_email": "johnsmith@gmail.com", + "user_email": "johnsmith@example.com", "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41, @@ -213,7 +233,7 @@ Please refer to `group_rename` and `user_rename` for that case. "project_name": "StoreCloud", "project_path": "storecloud", "project_path_with_namespace": "jsmith/storecloud", - "user_email": "johnsmith@gmail.com", + "user_email": "johnsmith@example.com", "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41, @@ -360,7 +380,7 @@ If the user is blocked via LDAP, `state` is `ldap_blocked`. "group_id": 78, "group_name": "StoreCloud", "group_path": "storecloud", - "user_email": "johnsmith@gmail.com", + "user_email": "johnsmith@example.com", "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41 @@ -378,7 +398,7 @@ If the user is blocked via LDAP, `state` is `ldap_blocked`. "group_id": 78, "group_name": "StoreCloud", "group_path": "storecloud", - "user_email": "johnsmith@gmail.com", + "user_email": "johnsmith@example.com", "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41 @@ -396,7 +416,7 @@ If the user is blocked via LDAP, `state` is `ldap_blocked`. "group_id": 78, "group_name": "StoreCloud", "group_path": "storecloud", - "user_email": "johnsmith@gmail.com", + "user_email": "johnsmith@example.com", "user_name": "John Smith", "user_username": "johnsmith", "user_id": 41 diff --git a/lib/api/ci/job_artifacts.rb b/lib/api/ci/job_artifacts.rb index ca76d2664f8..9f59eea5013 100644 --- a/lib/api/ci/job_artifacts.rb +++ b/lib/api/ci/job_artifacts.rb @@ -47,7 +47,7 @@ module API requires :artifact_path, type: String, desc: 'Artifact path' end route_setting :authentication, job_token_allowed: true - get ':id/jobs/artifacts/:ref_name/raw/*artifact_path', + get ':id/jobs/artifacts/:ref_name/raw/*artifact_path', urgency: :low, format: false, requirements: { ref_name: /.+/ } do authorize_download_artifacts! @@ -70,7 +70,7 @@ module API requires :job_id, type: Integer, desc: 'The ID of a job' end route_setting :authentication, job_token_allowed: true - get ':id/jobs/:job_id/artifacts' do + get ':id/jobs/:job_id/artifacts', urgency: :low do authorize_download_artifacts! build = find_build!(params[:job_id]) diff --git a/lib/api/ci/pipelines.rb b/lib/api/ci/pipelines.rb index 4e5d6c264bf..e0086f624a8 100644 --- a/lib/api/ci/pipelines.rb +++ b/lib/api/ci/pipelines.rb @@ -179,7 +179,7 @@ module API params do requires :pipeline_id, type: Integer, desc: 'The pipeline ID' end - get ':id/pipelines/:pipeline_id/test_report', feature_category: :code_testing do + get ':id/pipelines/:pipeline_id/test_report', feature_category: :code_testing, urgency: :low do authorize! :read_build, pipeline present pipeline.test_reports, with: TestReportEntity, details: true diff --git a/lib/api/ci/runner.rb b/lib/api/ci/runner.rb index 153738a8edc..9dcb10dd5e2 100644 --- a/lib/api/ci/runner.rb +++ b/lib/api/ci/runner.rb @@ -244,7 +244,7 @@ module API optional :artifact_type, type: String, desc: %q(The type of artifact), default: 'archive', values: ::Ci::JobArtifact.file_types.keys end - post '/:id/artifacts/authorize', feature_category: :build_artifacts do + post '/:id/artifacts/authorize', feature_category: :build_artifacts, urgency: :low do not_allowed! unless Gitlab.config.artifacts.enabled require_gitlab_workhorse! @@ -280,7 +280,7 @@ module API default: 'zip', values: ::Ci::JobArtifact.file_formats.keys optional :metadata, type: ::API::Validations::Types::WorkhorseFile, desc: %(The artifact metadata to store (generated by Multipart middleware)) end - post '/:id/artifacts', feature_category: :build_artifacts do + post '/:id/artifacts', feature_category: :build_artifacts, urgency: :low do not_allowed! unless Gitlab.config.artifacts.enabled require_gitlab_workhorse! diff --git a/lib/gitlab/hook_data/project_builder.rb b/lib/gitlab/hook_data/project_builder.rb index 65c237f743f..ebd97d3ab1b 100644 --- a/lib/gitlab/hook_data/project_builder.rb +++ b/lib/gitlab/hook_data/project_builder.rb @@ -16,6 +16,7 @@ module Gitlab # project_id: 1, # owner_name: "John", # owner_email: "user1@example.org", + # owners: [name: "John", email: "user1@example.org"], # project_visibility: "internal", # old_path_with_namespace: "old-path-with-namespace" # } @@ -32,19 +33,33 @@ module Gitlab private def project_data - owner = project.owner + owners = project.owners.compact + # When this is removed, also remove the `deprecated_owner` method + # See https://gitlab.com/gitlab-org/gitlab/-/issues/350603 + owner = project.deprecated_owner { name: project.name, path: project.path, path_with_namespace: project.full_path, project_id: project.id, - owner_name: owner.name, - owner_email: owner.respond_to?(:email) ? owner.email : "", + owner_name: owner.try(:name), + owner_email: user_email(owner), + owners: owners.map do |owner| + owner_data(owner) + end, project_visibility: project.visibility.downcase } end + def owner_data(user) + { name: user.name, email: user_email(user) } + end + + def user_email(user) + user.respond_to?(:email) ? user.email : "" + end + def event_specific_project_data(event) return {} unless event == :rename || event == :transfer diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index d91c1b0d29a..bf0b833b311 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -502,10 +502,7 @@ RSpec.describe Projects::IssuesController do context 'with valid params' do it 'reorders issues and returns a successful 200 response' do - reorder_issue(issue1, - move_after_id: issue2.id, - move_before_id: issue3.id, - group_full_path: group.full_path) + reorder_issue(issue1, move_after_id: issue2.id, move_before_id: issue3.id) [issue1, issue2, issue3].map(&:reload) @@ -531,12 +528,10 @@ RSpec.describe Projects::IssuesController do end it 'returns a unprocessable entity 422 response for issues not in group' do - another_group = create(:group) + other_group_project = create(:project, group: create(:group)) + other_group_issue = create(:issue, project: other_group_project) - reorder_issue(issue1, - move_after_id: issue2.id, - move_before_id: issue3.id, - group_full_path: another_group.full_path) + reorder_issue(issue1, move_after_id: issue2.id, move_before_id: other_group_issue.id) expect(response).to have_gitlab_http_status(:unprocessable_entity) end @@ -555,15 +550,14 @@ RSpec.describe Projects::IssuesController do end end - def reorder_issue(issue, move_after_id: nil, move_before_id: nil, group_full_path: nil) + def reorder_issue(issue, move_after_id: nil, move_before_id: nil) put :reorder, params: { namespace_id: project.namespace.to_param, project_id: project, id: issue.iid, move_after_id: move_after_id, - move_before_id: move_before_id, - group_full_path: group_full_path + move_before_id: move_before_id }, format: :json end diff --git a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb index 793a5bced00..22a27b33671 100644 --- a/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb +++ b/spec/features/admin/integrations/user_activates_mattermost_slash_command_spec.rb @@ -19,19 +19,4 @@ RSpec.describe 'User activates the instance-level Mattermost Slash Command integ expect(page).to have_link('Settings', href: edit_path) expect(page).to have_link('Projects using custom settings', href: overrides_path) end - - it 'does not render integration form element' do - expect(page).not_to have_selector('[data-testid="integration-form"]') - end - - context 'when `vue_integration_form` feature flag is disabled' do - before do - stub_feature_flags(vue_integration_form: false) - visit_instance_integration('Mattermost slash commands') - end - - it 'renders integration form element' do - expect(page).to have_selector('[data-testid="integration-form"]') - end - end end diff --git a/spec/features/merge_request/batch_comments_spec.rb b/spec/features/merge_request/batch_comments_spec.rb index f695b225915..a4aae7d8020 100644 --- a/spec/features/merge_request/batch_comments_spec.rb +++ b/spec/features/merge_request/batch_comments_spec.rb @@ -64,7 +64,11 @@ RSpec.describe 'Merge request > Batch comments', :js do it 'deletes draft note' do write_diff_comment - accept_alert { find('.js-note-delete').click } + find('.js-note-delete').click + + page.within('.modal') do + click_button('Delete Comment', match: :first) + end wait_for_requests diff --git a/spec/features/merge_request/user_comments_on_diff_spec.rb b/spec/features/merge_request/user_comments_on_diff_spec.rb index 8f7ad9bd90e..29c346593d5 100644 --- a/spec/features/merge_request/user_comments_on_diff_spec.rb +++ b/spec/features/merge_request/user_comments_on_diff_spec.rb @@ -238,8 +238,11 @@ RSpec.describe 'User comments on a diff', :js do page.within('.diff-file:nth-of-type(1) .discussion .note') do find('.more-actions').click find('.more-actions .dropdown-menu li', match: :first) + find('.js-note-delete').click + end - accept_confirm { find('.js-note-delete').click } + page.within('.modal') do + click_button('Delete Comment', match: :first) end page.within('.merge-request-tabs') do diff --git a/spec/features/merge_request/user_posts_diff_notes_spec.rb b/spec/features/merge_request/user_posts_diff_notes_spec.rb index 9e314e18563..76431d04e72 100644 --- a/spec/features/merge_request/user_posts_diff_notes_spec.rb +++ b/spec/features/merge_request/user_posts_diff_notes_spec.rb @@ -103,7 +103,7 @@ RSpec.describe 'Merge request > User posts diff notes', :js do it 'allows commenting' do should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]')) - accept_confirm do + accept_gl_confirm(button_text: 'Delete Comment') do first('button.more-actions-toggle').click first('.js-note-delete').click end diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb index 0416474218f..393108d8407 100644 --- a/spec/features/merge_request/user_posts_notes_spec.rb +++ b/spec/features/merge_request/user_posts_notes_spec.rb @@ -165,11 +165,13 @@ RSpec.describe 'Merge request > User posts notes', :js do it 'resets the edit note form textarea with the original content of the note if cancelled' do within('.current-note-edit-form') do fill_in 'note[note]', with: 'Some new content' - - accept_confirm do - find('[data-testid="cancel"]').click - end + find('[data-testid="cancel"]').click end + + page.within('.modal') do + click_button('OK', match: :first) + end + expect(find('.js-note-text').text).to eq '' end diff --git a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb index 64cd5aa2bb1..261d1ad325b 100644 --- a/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb +++ b/spec/features/merge_request/user_sees_avatar_on_diff_notes_spec.rb @@ -1,9 +1,11 @@ # frozen_string_literal: true require 'spec_helper' +include Spec::Support::Helpers::ModalHelpers # rubocop:disable Style/MixinUsage RSpec.describe 'Merge request > User sees avatars on diff notes', :js do include NoteInteractionHelpers + include Spec::Support::Helpers::ModalHelpers let(:project) { create(:project, :public, :repository) } let(:user) { project.creator } @@ -121,8 +123,8 @@ RSpec.describe 'Merge request > User sees avatars on diff notes', :js do it 'removes avatar when note is deleted' do open_more_actions_dropdown(note) - page.within find(".note-row-#{note.id}") do - accept_confirm { find('.js-note-delete').click } + accept_gl_confirm(button_text: 'Delete Comment') do + find(".note-row-#{note.id} .js-note-delete").click end wait_for_requests diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js index 2f123508c06..7e01b79383a 100644 --- a/spec/frontend/integrations/edit/components/integration_form_spec.js +++ b/spec/frontend/integrations/edit/components/integration_form_spec.js @@ -33,13 +33,6 @@ describe('IntegrationForm', () => { let wrapper; let dispatch; let mockAxios; - let mockForm; - let vueIntegrationFormFeatureFlag; - - const createForm = () => { - mockForm = document.createElement('form'); - jest.spyOn(document, 'querySelector').mockReturnValue(mockForm); - }; const createComponent = ({ customStateProps = {}, @@ -53,10 +46,6 @@ describe('IntegrationForm', () => { }); dispatch = jest.spyOn(store, 'dispatch').mockImplementation(); - if (!vueIntegrationFormFeatureFlag) { - createForm(); - } - wrapper = mountFn(IntegrationForm, { propsData: { ...props }, store, @@ -72,11 +61,6 @@ describe('IntegrationForm', () => { show: mockToastShow, }, }, - provide: { - glFeatures: { - vueIntegrationForm: vueIntegrationFormFeatureFlag, - }, - }, }); }; @@ -93,14 +77,8 @@ describe('IntegrationForm', () => { const findTriggerFields = () => wrapper.findComponent(TriggerFields); const findGlForm = () => wrapper.findComponent(GlForm); const findRedirectToField = () => wrapper.findByTestId('redirect-to-field'); - const findFormElement = () => (vueIntegrationFormFeatureFlag ? findGlForm().element : mockForm); const findDynamicField = () => wrapper.findComponent(DynamicField); - const mockFormFunctions = ({ checkValidityReturn }) => { - jest.spyOn(findFormElement(), 'checkValidity').mockReturnValue(checkValidityReturn); - jest.spyOn(findFormElement(), 'submit'); - }; - beforeEach(() => { mockAxios = new MockAdapter(axios); }); @@ -355,17 +333,14 @@ describe('IntegrationForm', () => { }); }); - describe('when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled', () => { - it('renders hidden fields', () => { - vueIntegrationFormFeatureFlag = true; - createComponent({ - customStateProps: { - redirectTo: '/services', - }, - }); - - expect(findRedirectToField().attributes('value')).toBe('/services'); + it('renders hidden fields', () => { + createComponent({ + customStateProps: { + redirectTo: '/services', + }, }); + + expect(findRedirectToField().attributes('value')).toBe('/services'); }); }); @@ -387,218 +362,200 @@ describe('IntegrationForm', () => { }); describe.each` - formActive | vueIntegrationFormEnabled | novalidate - ${true} | ${true} | ${null} - ${false} | ${true} | ${'novalidate'} - ${true} | ${false} | ${null} - ${false} | ${false} | ${'true'} + formActive | novalidate + ${true} | ${undefined} + ${false} | ${'true'} `( - 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled and `toggle-integration-active` is emitted with $formActive', - ({ formActive, vueIntegrationFormEnabled, novalidate }) => { + 'when `toggle-integration-active` is emitted with $formActive', + ({ formActive, novalidate }) => { beforeEach(async () => { - vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled; - createComponent({ customStateProps: { showActive: true, initialActivated: false, }, - mountFn: mountExtended, }); - mockFormFunctions({ checkValidityReturn: false }); await findActiveCheckbox().vm.$emit('toggle-integration-active', formActive); }); it(`sets noValidate to ${novalidate}`, () => { - expect(findFormElement().getAttribute('novalidate')).toBe(novalidate); + expect(findGlForm().attributes('novalidate')).toBe(novalidate); }); }, ); }); - describe.each` - vueIntegrationFormEnabled - ${true} - ${false} - `( - 'when `vueIntegrationForm` feature flag is $vueIntegrationFormEnabled', - ({ vueIntegrationFormEnabled }) => { - beforeEach(() => { - vueIntegrationFormFeatureFlag = vueIntegrationFormEnabled; - }); - - describe('when `save` button is clicked', () => { - describe('buttons', () => { - beforeEach(async () => { - createComponent({ - customStateProps: { - showActive: true, - canTest: true, - initialActivated: true, - }, - mountFn: mountExtended, - }); - - await findProjectSaveButton().vm.$emit('click', new Event('click')); - }); - - it('sets save button `loading` prop to `true`', () => { - expect(findProjectSaveButton().props('loading')).toBe(true); - }); - - it('sets test button `disabled` prop to `true`', () => { - expect(findTestButton().props('disabled')).toBe(true); - }); - }); - - describe.each` - checkValidityReturn | integrationActive - ${true} | ${false} - ${true} | ${true} - ${false} | ${false} - `( - 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)', - ({ integrationActive, checkValidityReturn }) => { - beforeEach(async () => { - createComponent({ - customStateProps: { - showActive: true, - canTest: true, - initialActivated: integrationActive, - }, - mountFn: mountExtended, - }); - - mockFormFunctions({ checkValidityReturn }); - - await findProjectSaveButton().vm.$emit('click', new Event('click')); - }); - - it('submits form', () => { - expect(findFormElement().submit).toHaveBeenCalledTimes(1); - }); + describe('when `save` button is clicked', () => { + describe('buttons', () => { + beforeEach(async () => { + createComponent({ + customStateProps: { + showActive: true, + canTest: true, + initialActivated: true, }, - ); + mountFn: mountExtended, + }); - describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => { - beforeEach(async () => { - createComponent({ - customStateProps: { - showActive: true, - canTest: true, - initialActivated: true, - fields: [mockField], - }, - mountFn: mountExtended, - }); - mockFormFunctions({ checkValidityReturn: false }); + await findProjectSaveButton().vm.$emit('click', new Event('click')); + }); - await findProjectSaveButton().vm.$emit('click', new Event('click')); + it('sets save button `loading` prop to `true`', () => { + expect(findProjectSaveButton().props('loading')).toBe(true); + }); + + it('sets test button `disabled` prop to `true`', () => { + expect(findTestButton().props('disabled')).toBe(true); + }); + }); + + describe.each` + checkValidityReturn | integrationActive + ${true} | ${false} + ${true} | ${true} + ${false} | ${false} + `( + 'when form is valid (checkValidity returns $checkValidityReturn and integrationActive is $integrationActive)', + ({ integrationActive, checkValidityReturn }) => { + beforeEach(async () => { + createComponent({ + customStateProps: { + showActive: true, + canTest: true, + initialActivated: integrationActive, + }, + mountFn: mountExtended, }); + jest.spyOn(findGlForm().element, 'submit'); + jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(checkValidityReturn); - it('does not submit form', () => { - expect(findFormElement().submit).not.toHaveBeenCalled(); - }); + await findProjectSaveButton().vm.$emit('click', new Event('click')); + }); - it('sets save button `loading` prop to `false`', () => { - expect(findProjectSaveButton().props('loading')).toBe(false); - }); + it('submit form', () => { + expect(findGlForm().element.submit).toHaveBeenCalledTimes(1); + }); + }, + ); - it('sets test button `disabled` prop to `false`', () => { - expect(findTestButton().props('disabled')).toBe(false); - }); + describe('when form is invalid (checkValidity returns false and integrationActive is true)', () => { + beforeEach(async () => { + createComponent({ + customStateProps: { + showActive: true, + canTest: true, + initialActivated: true, + fields: [mockField], + }, + mountFn: mountExtended, + }); + jest.spyOn(findGlForm().element, 'submit'); + jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false); - it('sets `isValidated` props on form fields', () => { - expect(findDynamicField().props('isValidated')).toBe(true); - }); + await findProjectSaveButton().vm.$emit('click', new Event('click')); + }); + + it('does not submit form', () => { + expect(findGlForm().element.submit).not.toHaveBeenCalled(); + }); + + it('sets save button `loading` prop to `false`', () => { + expect(findProjectSaveButton().props('loading')).toBe(false); + }); + + it('sets test button `disabled` prop to `false`', () => { + expect(findTestButton().props('disabled')).toBe(false); + }); + + it('sets `isValidated` props on form fields', () => { + expect(findDynamicField().props('isValidated')).toBe(true); + }); + }); + }); + + describe('when `test` button is clicked', () => { + describe('when form is invalid', () => { + it('sets `isValidated` props on form fields', async () => { + createComponent({ + customStateProps: { + showActive: true, + canTest: true, + fields: [mockField], + }, + mountFn: mountExtended, + }); + jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(false); + + await findTestButton().vm.$emit('click', new Event('click')); + + expect(findDynamicField().props('isValidated')).toBe(true); + }); + }); + + describe('when form is valid', () => { + const mockTestPath = '/test'; + + beforeEach(() => { + createComponent({ + customStateProps: { + showActive: true, + canTest: true, + testPath: mockTestPath, + }, + mountFn: mountExtended, + }); + jest.spyOn(findGlForm().element, 'checkValidity').mockReturnValue(true); + }); + + describe('buttons', () => { + beforeEach(async () => { + await findTestButton().vm.$emit('click', new Event('click')); + }); + + it('sets test button `loading` prop to `true`', () => { + expect(findTestButton().props('loading')).toBe(true); + }); + + it('sets save button `disabled` prop to `true`', () => { + expect(findProjectSaveButton().props('disabled')).toBe(true); }); }); - describe('when `test` button is clicked', () => { - describe('when form is invalid', () => { - it('sets `isValidated` props on form fields', async () => { - createComponent({ - customStateProps: { - showActive: true, - canTest: true, - fields: [mockField], - }, - mountFn: mountExtended, - }); - mockFormFunctions({ checkValidityReturn: false }); - - await findTestButton().vm.$emit('click', new Event('click')); - - expect(findDynamicField().props('isValidated')).toBe(true); + describe.each` + scenario | replyStatus | errorMessage | expectToast | expectSentry + ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true} + ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false} + ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false} + `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => { + beforeEach(async () => { + mockAxios.onPut(mockTestPath).replyOnce(replyStatus, { + error: Boolean(errorMessage), + message: errorMessage, }); + + await findTestButton().vm.$emit('click', new Event('click')); + await waitForPromises(); }); - describe('when form is valid', () => { - const mockTestPath = '/test'; + it(`calls toast with '${expectToast}'`, () => { + expect(mockToastShow).toHaveBeenCalledWith(expectToast); + }); - beforeEach(() => { - createComponent({ - customStateProps: { - showActive: true, - canTest: true, - testPath: mockTestPath, - }, - mountFn: mountExtended, - }); - mockFormFunctions({ checkValidityReturn: true }); - }); + it('sets `loading` prop of test button to `false`', () => { + expect(findTestButton().props('loading')).toBe(false); + }); - describe('buttons', () => { - beforeEach(async () => { - await findTestButton().vm.$emit('click', new Event('click')); - }); + it('sets save button `disabled` prop to `false`', () => { + expect(findProjectSaveButton().props('disabled')).toBe(false); + }); - it('sets test button `loading` prop to `true`', () => { - expect(findTestButton().props('loading')).toBe(true); - }); - - it('sets save button `disabled` prop to `true`', () => { - expect(findProjectSaveButton().props('disabled')).toBe(true); - }); - }); - - describe.each` - scenario | replyStatus | errorMessage | expectToast | expectSentry - ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true} - ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${'an error'} | ${false} - ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false} - `('$scenario', ({ replyStatus, errorMessage, expectToast, expectSentry }) => { - beforeEach(async () => { - mockAxios.onPut(mockTestPath).replyOnce(replyStatus, { - error: Boolean(errorMessage), - message: errorMessage, - }); - - await findTestButton().vm.$emit('click', new Event('click')); - await waitForPromises(); - }); - - it(`calls toast with '${expectToast}'`, () => { - expect(mockToastShow).toHaveBeenCalledWith(expectToast); - }); - - it('sets `loading` prop of test button to `false`', () => { - expect(findTestButton().props('loading')).toBe(false); - }); - - it('sets save button `disabled` prop to `false`', () => { - expect(findProjectSaveButton().props('disabled')).toBe(false); - }); - - it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => { - expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0); - }); - }); + it(`${expectSentry ? 'does' : 'does not'} capture exception in Sentry`, () => { + expect(Sentry.captureException).toHaveBeenCalledTimes(expectSentry ? 1 : 0); }); }); - }, - ); + }); + }); describe('when `reset-confirmation-modal` emits `reset` event', () => { const mockResetPath = '/reset'; diff --git a/spec/lib/gitlab/hook_data/project_builder_spec.rb b/spec/lib/gitlab/hook_data/project_builder_spec.rb index 672dbab918f..e86ac66b1ad 100644 --- a/spec/lib/gitlab/hook_data/project_builder_spec.rb +++ b/spec/lib/gitlab/hook_data/project_builder_spec.rb @@ -13,7 +13,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do let(:attributes) do [ :event_name, :created_at, :updated_at, :name, :path, :path_with_namespace, :project_id, - :owner_name, :owner_email, :project_visibility + :owners, :owner_name, :owner_email, :project_visibility ] end @@ -30,6 +30,7 @@ RSpec.describe Gitlab::HookData::ProjectBuilder do expect(data[:project_id]).to eq(project.id) expect(data[:owner_name]).to eq('John') expect(data[:owner_email]).to eq('john@example.com') + expect(data[:owners]).to contain_exactly({ name: 'John', email: 'john@example.com' }) expect(data[:project_visibility]).to eq('internal') end end diff --git a/spec/models/namespaces/user_namespace_spec.rb b/spec/models/namespaces/user_namespace_spec.rb index 7c00a597756..fb9e7571666 100644 --- a/spec/models/namespaces/user_namespace_spec.rb +++ b/spec/models/namespaces/user_namespace_spec.rb @@ -9,4 +9,13 @@ RSpec.describe Namespaces::UserNamespace, type: :model do describe 'validations' do it { is_expected.to validate_presence_of(:owner) } end + + describe '#owners' do + let(:owner) { build(:user) } + let(:namespace) { build(:namespace, owner: owner) } + + specify do + expect(namespace.owners).to match_array([owner]) + end + end end diff --git a/spec/requests/api/issues/issues_spec.rb b/spec/requests/api/issues/issues_spec.rb index e15f847d866..c5e57b5b18b 100644 --- a/spec/requests/api/issues/issues_spec.rb +++ b/spec/requests/api/issues/issues_spec.rb @@ -1184,14 +1184,15 @@ RSpec.describe API::Issues do end describe 'PUT /projects/:id/issues/:issue_iid/reorder' do - let_it_be(:project) { create(:project) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } let_it_be(:issue1) { create(:issue, project: project, relative_position: 10) } let_it_be(:issue2) { create(:issue, project: project, relative_position: 20) } let_it_be(:issue3) { create(:issue, project: project, relative_position: 30) } context 'when user has access' do - before do - project.add_developer(user) + before_all do + group.add_developer(user) end context 'with valid params' do @@ -1217,6 +1218,19 @@ RSpec.describe API::Issues do expect(response).to have_gitlab_http_status(:not_found) end end + + context 'with issue in different project' do + let(:other_project) { create(:project, group: group) } + let(:other_issue) { create(:issue, project: other_project, relative_position: 80) } + + it 'reorders issues and returns a successful 200 response' do + put api("/projects/#{other_project.id}/issues/#{other_issue.iid}/reorder", user), params: { move_after_id: issue2.id, move_before_id: issue3.id } + + expect(response).to have_gitlab_http_status(:ok) + expect(other_issue.reload.relative_position) + .to be_between(issue2.reload.relative_position, issue3.reload.relative_position) + end + end end context 'with unauthorized user' do diff --git a/spec/services/issues/reorder_service_spec.rb b/spec/services/issues/reorder_service_spec.rb index 15668a3aa23..392930c1b9f 100644 --- a/spec/services/issues/reorder_service_spec.rb +++ b/spec/services/issues/reorder_service_spec.rb @@ -67,20 +67,10 @@ RSpec.describe Issues::ReorderService do it_behaves_like 'issues reorder service' context 'when ordering in a group issue list' do - let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id, group_full_path: group.full_path } } + let(:params) { { move_after_id: issue2.id, move_before_id: issue3.id } } subject { service(params) } - it 'sends the board_group_id parameter' do - match_params = { move_between_ids: [issue2.id, issue3.id], board_group_id: group.id } - - expect(Issues::UpdateService) - .to receive(:new).with(project: project, current_user: user, params: match_params) - .and_return(double(execute: build(:issue))) - - subject.execute(issue1) - end - it 'sorts issues' do project2 = create(:project, namespace: group) issue4 = create(:issue, project: project2) diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb index c53231c7042..95394ba6597 100644 --- a/spec/services/issues/update_service_spec.rb +++ b/spec/services/issues/update_service_spec.rb @@ -385,7 +385,6 @@ RSpec.describe Issues::UpdateService, :mailer do [issue_1, issue_2, issue_3].map(&:save) opts[:move_between_ids] = [issue_1.id, issue_2.id] - opts[:board_group_id] = group.id described_class.new(project: issue_3.project, current_user: user, params: opts).execute(issue_3) expect(issue_2.relative_position).to be_between(issue_1.relative_position, issue_2.relative_position) @@ -1309,19 +1308,12 @@ RSpec.describe Issues::UpdateService, :mailer do end context 'when moving an issue ' do - it 'raises an error for invalid move ids within a project' do + it 'raises an error for invalid move ids' do opts = { move_between_ids: [9000, non_existing_record_id] } expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) } .to raise_error(ActiveRecord::RecordNotFound) end - - it 'raises an error for invalid move ids within a group' do - opts = { move_between_ids: [9000, non_existing_record_id], board_group_id: create(:group).id } - - expect { described_class.new(project: issue.project, current_user: user, params: opts).execute(issue) } - .to raise_error(ActiveRecord::RecordNotFound) - end end include_examples 'issuable update service' do diff --git a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb index d9b837258ce..a46c2f0ac5c 100644 --- a/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb +++ b/spec/support/shared_examples/services/boards/issues_move_service_shared_examples.rb @@ -140,19 +140,6 @@ RSpec.shared_examples 'issues move service' do |group| expect(issue2.reload.updated_at.change(usec: 0)).to eq updated_at2.change(usec: 0) end - if group - context 'when on a group board' do - it 'sends the board_group_id parameter' do - params.merge!(move_after_id: issue1.id, move_before_id: issue2.id) - - match_params = { move_between_ids: [issue1.id, issue2.id], board_group_id: parent.id } - expect(Issues::UpdateService).to receive(:new).with(project: issue.project, current_user: user, params: match_params).and_return(double(execute: build(:issue))) - - described_class.new(parent, user, params).execute(issue) - end - end - end - def reorder_issues(params, issues: []) issues.each do |issue| issue.move_to_end && issue.save! diff --git a/spec/views/projects/services/_form.haml_spec.rb b/spec/views/projects/services/_form.haml_spec.rb deleted file mode 100644 index f212fd78b1a..00000000000 --- a/spec/views/projects/services/_form.haml_spec.rb +++ /dev/null @@ -1,52 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'projects/services/_form' do - let(:project) { create(:redmine_project) } - let(:user) { create(:admin) } - - before do - assign(:project, project) - - allow(controller).to receive(:current_user).and_return(user) - - allow(view).to receive_messages( - current_user: user, - can?: true, - current_application_settings: Gitlab::CurrentSettings.current_application_settings, - integration: project.redmine_integration, - request: double(referer: '/services') - ) - end - - context 'integrations form' do - it 'does not render form element' do - render - - expect(rendered).not_to have_selector('[data-testid="integration-form"]') - end - - context 'when vue_integration_form feature flag is disabled' do - before do - stub_feature_flags(vue_integration_form: false) - end - - it 'renders form element' do - render - - expect(rendered).to have_selector('[data-testid="integration-form"]') - end - - context 'commit_events and merge_request_events' do - it 'display merge_request_events and commit_events descriptions' do - allow(Integrations::Redmine).to receive(:supported_events).and_return(%w(commit merge_request)) - - render - - expect(rendered).to have_css("input[name='redirect_to'][value='/services']", count: 1, visible: false) - end - end - end - end -end