diff options
32 files changed, 936 insertions, 694 deletions
diff --git a/app/assets/javascripts/boards/components/board_card_inner.vue b/app/assets/javascripts/boards/components/board_card_inner.vue index 05b64ddc773..5658a34e9a6 100644 --- a/app/assets/javascripts/boards/components/board_card_inner.vue +++ b/app/assets/javascripts/boards/components/board_card_inner.vue @@ -65,7 +65,7 @@ export default { }, computed: { ...mapState(['isShowingLabels', 'issuableType', 'allowSubEpics']), - ...mapGetters(['isEpicBoard']), + ...mapGetters(['isEpicBoard', 'isProjectBoard']), cappedAssignees() { // e.g. maxRender is 4, // Render up to all 4 assignees if there are only 4 assigness @@ -144,6 +144,9 @@ export default { totalProgress() { return Math.round((this.item.descendantWeightSum.closedIssues / this.totalWeight) * 100); }, + showReferencePath() { + return !this.isProjectBoard && this.itemReferencePath; + }, }, methods: { ...mapActions(['performSearch', 'setError']), @@ -247,7 +250,7 @@ export default { :class="{ 'gl-font-base': isEpicBoard }" > <tooltip-on-truncate - v-if="itemReferencePath" + v-if="showReferencePath" :title="itemReferencePath" placement="bottom" class="board-item-path gl-text-truncate gl-font-weight-bold" diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 2664a7b7151..7ee680db7f9 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -4,6 +4,9 @@ module IssuableActions extend ActiveSupport::Concern include Gitlab::Utils::StrongMemoize include Gitlab::Cache::Helpers + include SpammableActions::AkismetMarkAsSpamAction + include SpammableActions::CaptchaCheck::HtmlFormatActionsSupport + include SpammableActions::CaptchaCheck::JsonFormatActionsSupport included do before_action :authorize_destroy_issuable!, only: :destroy @@ -25,17 +28,42 @@ module IssuableActions end def update - @issuable = update_service.execute(issuable) # rubocop:disable Gitlab/ModuleWithInstanceVariables - respond_to do |format| - format.html do - recaptcha_check_if_spammable { render :edit } + updated_issuable = update_service.execute(issuable) + # NOTE: We only assign the instance variable on this line, and use the local variable + # everywhere else in the method, to avoid having to add multiple `rubocop:disable` comments. + @issuable = updated_issuable # rubocop:disable Gitlab/ModuleWithInstanceVariables + + # NOTE: This check for `is_a?(Spammable)` is necessary because not all + # possible `issuable` types implement Spammable. Once they all implement Spammable, + # this check can be removed. + if updated_issuable.is_a?(Spammable) + respond_to do |format| + format.html do + # NOTE: This redirect is intentionally only performed in the case where the updated + # issuable is a spammable, and intentionally is not performed in the non-spammable case. + # This preserves the legacy behavior of this action. + if updated_issuable.valid? + redirect_to spammable_path + else + with_captcha_check_html_format { render :edit } + end + end + + format.json do + with_captcha_check_json_format { render_entity_json } + end end - - format.json do - recaptcha_check_if_spammable(false) { render_entity_json } + else + respond_to do |format| + format.html do + render :edit + end + + format.json do + render_entity_json + end end end - rescue ActiveRecord::StaleObjectError render_conflict_response end @@ -171,12 +199,6 @@ module IssuableActions DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user, note_entity: ProjectNoteEntity) end - def recaptcha_check_if_spammable(should_redirect = true, &block) - return yield unless issuable.is_a? Spammable - - recaptcha_check_with_fallback(should_redirect, &block) - end - def render_conflict_response respond_to do |format| format.html do diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb deleted file mode 100644 index eb1223f22a9..00000000000 --- a/app/controllers/concerns/spammable_actions.rb +++ /dev/null @@ -1,73 +0,0 @@ -# frozen_string_literal: true - -module SpammableActions - extend ActiveSupport::Concern - include Spam::Concerns::HasSpamActionResponseFields - - included do - before_action :authorize_submit_spammable!, only: :mark_as_spam - end - - def mark_as_spam - if Spam::MarkAsSpamService.new(target: spammable).execute - redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase } - else - redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.') - end - end - - private - - def recaptcha_check_with_fallback(should_redirect = true, &fallback) - if should_redirect && spammable.valid? - redirect_to spammable_path - elsif spammable.render_recaptcha? - Gitlab::Recaptcha.load_configurations! - - respond_to do |format| - format.html do - # NOTE: format.html is still used by issue create, and uses the legacy HAML - # `_recaptcha_form.html.haml` rendered via the `projects/issues/verify` template. - render :verify - end - - format.json do - # format.json is used by all new Vue-based CAPTCHA implementations, which - # handle all of the CAPTCHA form rendering on the client via the Pajamas-based - # app/assets/javascripts/captcha/captcha_modal.vue - - # NOTE: "409 - Conflict" seems to be the most appropriate HTTP status code for a response - # which requires a CAPTCHA to be solved in order for the request to be resubmitted. - # See https://stackoverflow.com/q/26547466/25192 - render json: spam_action_response_fields(spammable), status: :conflict - end - end - else - yield - end - end - - # TODO: This method is currently only needed for issue create, to convert spam/CAPTCHA values from - # params, and instead be passed as headers, as the spam services now all expect. It can be removed - # when issue create is is converted to a client/JS based approach instead of the legacy HAML - # `_recaptcha_form.html.haml` which is rendered via the `projects/issues/verify` template. - # In that case, which is based on the legacy reCAPTCHA implementation using the HTML/HAML form, - # the 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the - # recaptcha gem, which is called from the HAML `_recaptcha_form.html.haml` form. - def extract_legacy_spam_params_to_headers - request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] || params[:captcha_response] - request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id] - end - - def spammable - raise NotImplementedError, "#{self.class} does not implement #{__method__}" - end - - def spammable_path - raise NotImplementedError, "#{self.class} does not implement #{__method__}" - end - - def authorize_submit_spammable! - access_denied! unless current_user.admin? - end -end diff --git a/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb b/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb new file mode 100644 index 00000000000..234c591ffb7 --- /dev/null +++ b/app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module SpammableActions::AkismetMarkAsSpamAction + extend ActiveSupport::Concern + include SpammableActions::Attributes + + included do + before_action :authorize_submit_spammable!, only: :mark_as_spam + end + + def mark_as_spam + if Spam::AkismetMarkAsSpamService.new(target: spammable).execute + redirect_to spammable_path, notice: _("%{spammable_titlecase} was submitted to Akismet successfully.") % { spammable_titlecase: spammable.spammable_entity_type.titlecase } + else + redirect_to spammable_path, alert: _('Error with Akismet. Please check the logs for more info.') + end + end + + private + + def authorize_submit_spammable! + access_denied! unless current_user.can_admin_all_resources? + end + + def spammable_path + raise NotImplementedError, "#{self.class} does not implement #{__method__}" + end +end diff --git a/app/controllers/concerns/spammable_actions/attributes.rb b/app/controllers/concerns/spammable_actions/attributes.rb new file mode 100644 index 00000000000..d7060e47c07 --- /dev/null +++ b/app/controllers/concerns/spammable_actions/attributes.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module SpammableActions + module Attributes + extend ActiveSupport::Concern + + private + + def spammable + raise NotImplementedError, "#{self.class} does not implement #{__method__}" + end + end +end diff --git a/app/controllers/concerns/spammable_actions/captcha_check/common.rb b/app/controllers/concerns/spammable_actions/captcha_check/common.rb new file mode 100644 index 00000000000..7c047e02a1d --- /dev/null +++ b/app/controllers/concerns/spammable_actions/captcha_check/common.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module SpammableActions::CaptchaCheck + module Common + extend ActiveSupport::Concern + + private + + def with_captcha_check_common(captcha_render_lambda:, &block) + # If the Spammable indicates that CAPTCHA is not necessary (either due to it not being flagged + # as spam, or if spam/captcha is disabled for some reason), then we will go ahead and + # yield to the block containing the action's original behavior, then return. + return yield unless spammable.render_recaptcha? + + # If we got here, we need to render the CAPTCHA instead of yielding to action's original + # behavior. We will present a CAPTCHA to be solved by executing the lambda which was passed + # as the `captcha_render_lambda:` argument. This lambda contains either the HTML-specific or + # JSON-specific behavior to cause the CAPTCHA modal to be rendered. + Gitlab::Recaptcha.load_configurations! + captcha_render_lambda.call + end + end +end diff --git a/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb b/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb new file mode 100644 index 00000000000..70b4c046128 --- /dev/null +++ b/app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +# This module should *ONLY* be included if needed to support forms submits with HTML MIME type. +# In other words, forms handled by actions which use a `respond_to` of `format.html`. +# +# If the request is handled by actions via `format.json`, for example, for all Javascript based form +# submissions and Vue components which use Apollo and Axios, then the corresponding module +# which supports JSON format should be used instead. +module SpammableActions::CaptchaCheck::HtmlFormatActionsSupport + extend ActiveSupport::Concern + include SpammableActions::Attributes + include SpammableActions::CaptchaCheck::Common + + included do + before_action :convert_html_spam_params_to_headers, only: [:create, :update] + end + + private + + def with_captcha_check_html_format(&block) + captcha_render_lambda = -> { render :verify } + with_captcha_check_common(captcha_render_lambda: captcha_render_lambda, &block) + end + + # Convert spam/CAPTCHA values from form field params to headers, because all spam-related services + # expect these values to be passed as headers. + # + # The 'g-recaptcha-response' field name comes from `Recaptcha::ClientHelper#recaptcha_tags` in the + # recaptcha gem. This is a field which is automatically included by calling the + # `#recaptcha_tags` method within a HAML template's form. + def convert_html_spam_params_to_headers + request.headers['X-GitLab-Captcha-Response'] = params['g-recaptcha-response'] if params['g-recaptcha-response'] + request.headers['X-GitLab-Spam-Log-Id'] = params[:spam_log_id] if params[:spam_log_id] + end +end diff --git a/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb b/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb new file mode 100644 index 00000000000..9fa448a9abb --- /dev/null +++ b/app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +# This module should be included to support forms submits with a 'js' or 'json' type of MIME type. +# In other words, forms handled by actions which use a `respond_to` of `format.js` or `format.json`. +# +# For example, for all Javascript based form submissions and Vue components which use Apollo and Axios +# +# If the request is handled by actions via `format.html`, then the corresponding module which +# supports HTML format should be used instead. +module SpammableActions::CaptchaCheck::JsonFormatActionsSupport + extend ActiveSupport::Concern + include SpammableActions::Attributes + include SpammableActions::CaptchaCheck::Common + include Spam::Concerns::HasSpamActionResponseFields + + private + + def with_captcha_check_json_format(&block) + # NOTE: "409 - Conflict" seems to be the most appropriate HTTP status code for a response + # which requires a CAPTCHA to be solved in order for the request to be resubmitted. + # See https://stackoverflow.com/q/26547466/25192 + captcha_render_lambda = -> { render json: spam_action_response_fields(spammable), status: :conflict } + with_captcha_check_common(captcha_render_lambda: captcha_render_lambda, &block) + end +end diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb index 5d38e431c8a..e3b2d452151 100644 --- a/app/controllers/projects/issues_controller.rb +++ b/app/controllers/projects/issues_controller.rb @@ -7,7 +7,6 @@ class Projects::IssuesController < Projects::ApplicationController include ToggleAwardEmoji include IssuableCollections include IssuesCalendar - include SpammableActions include RecordUserLastActivity ISSUES_EXCEPT_ACTIONS = %i[index calendar new create bulk_update import_csv export_csv service_desk].freeze @@ -129,7 +128,6 @@ class Projects::IssuesController < Projects::ApplicationController end def create - extract_legacy_spam_params_to_headers create_params = issue_params.merge( merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of], discussion_to_resolve: params[:discussion_to_resolve] @@ -149,10 +147,11 @@ class Projects::IssuesController < Projects::ApplicationController end end - respond_to do |format| - format.html do - recaptcha_check_with_fallback { render :new } - end + if @issue.valid? + redirect_to project_issue_path(@project, @issue) + else + # NOTE: this CAPTCHA support method is indirectly included via IssuableActions + with_captcha_check_html_format { render :new } end end diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb index 7d9c416e8f0..97f9c5814e2 100644 --- a/app/controllers/projects/snippets_controller.rb +++ b/app/controllers/projects/snippets_controller.rb @@ -4,7 +4,7 @@ class Projects::SnippetsController < Projects::Snippets::ApplicationController extend ::Gitlab::Utils::Override include SnippetsActions include ToggleAwardEmoji - include SpammableActions + include SpammableActions::AkismetMarkAsSpamAction before_action :check_snippets_available! diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb index 1c6168dbc2c..e81868faa6e 100644 --- a/app/controllers/snippets_controller.rb +++ b/app/controllers/snippets_controller.rb @@ -4,7 +4,7 @@ class SnippetsController < Snippets::ApplicationController include SnippetsActions include PreviewMarkdown include ToggleAwardEmoji - include SpammableActions + include SpammableActions::AkismetMarkAsSpamAction before_action :snippet, only: [:show, :edit, :raw, :toggle_award_emoji, :mark_as_spam] diff --git a/app/graphql/mutations/snippets/mark_as_spam.rb b/app/graphql/mutations/snippets/mark_as_spam.rb index d6e3e131b81..fd427f9b519 100644 --- a/app/graphql/mutations/snippets/mark_as_spam.rb +++ b/app/graphql/mutations/snippets/mark_as_spam.rb @@ -23,7 +23,7 @@ module Mutations private def mark_as_spam(snippet) - Spam::MarkAsSpamService.new(target: snippet).execute + Spam::AkismetMarkAsSpamService.new(target: snippet).execute end def authorized_resource?(snippet) diff --git a/app/services/spam/mark_as_spam_service.rb b/app/services/spam/akismet_mark_as_spam_service.rb index ed5e674d8e9..da5506b9a21 100644 --- a/app/services/spam/mark_as_spam_service.rb +++ b/app/services/spam/akismet_mark_as_spam_service.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Spam - class MarkAsSpamService + class AkismetMarkAsSpamService include ::AkismetMethods attr_accessor :target, :options @@ -9,12 +9,12 @@ module Spam def initialize(target:) @target = target @options = {} + end + def execute @options[:ip_address] = @target.ip_address @options[:user_agent] = @target.user_agent - end - def execute return unless target.submittable_as_spam? return unless akismet.submit_spam diff --git a/doc/api/events.md b/doc/api/events.md index 52d8f3b1511..3fbbfa62e66 100644 --- a/doc/api/events.md +++ b/doc/api/events.md @@ -83,7 +83,7 @@ YYYY-MM-DD ### Event Time Period Limit -GitLab removes events older than 2 years from the events table for performance reasons. +GitLab removes events older than 3 years from the events table for performance reasons. ## List currently authenticated user's events @@ -73,6 +73,7 @@ module QA autoload :GroupBase, 'qa/resource/group_base' autoload :Sandbox, 'qa/resource/sandbox' autoload :Group, 'qa/resource/group' + autoload :BulkImportGroup, 'qa/resource/bulk_import_group' autoload :Issue, 'qa/resource/issue' autoload :ProjectIssueNote, 'qa/resource/project_issue_note' autoload :Project, 'qa/resource/project' diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index 88b388bd2e0..2848e3ba7d2 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -188,7 +188,7 @@ module QA end def log_having_both_api_result_and_block(name, api_value) - QA::Runtime::Logger.info(<<~MSG.strip) + QA::Runtime::Logger.debug(<<~MSG.strip) <#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored. MSG end diff --git a/qa/qa/resource/bulk_import_group.rb b/qa/qa/resource/bulk_import_group.rb new file mode 100644 index 00000000000..5a7041d9f77 --- /dev/null +++ b/qa/qa/resource/bulk_import_group.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module QA + module Resource + class BulkImportGroup < Group + attributes :source_group_path, + :import_id + + attribute :destination_group_path do + source_group_path + end + + attribute :access_token do + api_client.personal_access_token + end + + alias_method :path, :source_group_path + + delegate :gitlab_address, to: 'QA::Runtime::Scenario' + + def fabricate_via_browser_ui! + Page::Main::Menu.perform(&:go_to_create_group) + + Page::Group::New.perform do |group| + group.switch_to_import_tab + group.connect_gitlab_instance(gitlab_address, api_client.personal_access_token) + end + + Page::Group::BulkImport.perform do |import_page| + import_page.import_group(path, sandbox.path) + end + + reload! + visit! + end + + def fabricate_via_api! + response = post(Runtime::API::Request.new(api_client, api_post_path).url, api_post_body) + @import_id = parse_body(response)[:id] + + "#{gitlab_address}/#{full_path}" + end + + def api_post_path + '/bulk_imports' + end + + def api_post_body + { + configuration: { + url: gitlab_address, + access_token: access_token + }, + entities: [ + { + source_type: 'group_entity', + source_full_path: source_group_path, + destination_name: destination_group_path, + destination_namespace: sandbox.path + } + ] + } + end + + def import_status + response = get(Runtime::API::Request.new(api_client, "/bulk_imports/#{import_id}").url) + + unless response.code == HTTP_STATUS_OK + raise ResourceQueryError, "Could not get import status. Request returned (#{response.code}): `#{response}`." + end + + parse_body(response)[:status] + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb new file mode 100644 index 00000000000..2ace3f13217 --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage', :requires_admin do + describe 'Bulk group import via api' do + let!(:staging?) { Runtime::Scenario.gitlab_address.include?('staging.gitlab.com') } + + let(:admin_api_client) { Runtime::API::Client.as_admin } + let(:user) do + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + usr.hard_delete_on_api_removal = true + end + end + + let(:api_client) { Runtime::API::Client.new(user: user) } + let(:personal_access_token) { api_client.personal_access_token } + + let(:sandbox) do + Resource::Sandbox.fabricate_via_api! do |group| + group.api_client = admin_api_client + end + end + + let(:source_group) do + Resource::Sandbox.fabricate_via_api! do |group| + group.api_client = api_client + group.path = "source-group-for-import-#{SecureRandom.hex(4)}" + end + end + + let(:subgroup) do + Resource::Group.fabricate_via_api! do |group| + group.api_client = api_client + group.sandbox = source_group + group.path = "subgroup-for-import-#{SecureRandom.hex(4)}" + end + end + + let(:imported_subgroup) do + Resource::Group.init do |group| + group.api_client = api_client + group.sandbox = imported_group + group.path = subgroup.path + end + end + + let(:imported_group) do + Resource::BulkImportGroup.fabricate_via_api! do |group| + group.api_client = api_client + group.sandbox = sandbox + group.source_group_path = source_group.path + end + end + + before do + Runtime::Feature.enable(:bulk_import) unless staging? + Runtime::Feature.enable(:top_level_group_creation_enabled) if staging? + + sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER) + + Resource::GroupLabel.fabricate_via_api! do |label| + label.api_client = api_client + label.group = source_group + label.title = "source-group-#{SecureRandom.hex(4)}" + end + Resource::GroupLabel.fabricate_via_api! do |label| + label.api_client = api_client + label.group = subgroup + label.title = "subgroup-#{SecureRandom.hex(4)}" + end + end + + # Non blocking issues: + # https://gitlab.com/gitlab-org/gitlab/-/issues/331252 + # https://gitlab.com/gitlab-org/gitlab/-/issues/333678 <- can cause 500 when creating user and group back to back + it( + 'imports group with subgroups and labels', + testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1871' + ) do + Page::Group::BulkImport.perform do |import_page| + imported_group + + expect { imported_group.import_status }.to eventually_eq('finished').within(duration: 300, interval: 2) + + aggregate_failures do + expect(imported_group.reload!).to eq(source_group) + expect(imported_group.labels).to include(*source_group.labels) + + expect(imported_subgroup.reload!).to eq(subgroup) + expect(imported_subgroup.labels).to include(*subgroup.labels) + end + end + end + + after do + user.remove_via_api! + ensure + Runtime::Feature.disable(:bulk_import) unless staging? + Runtime::Feature.disable(:top_level_group_creation_enabled) if staging? + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb index 4fee7e6d07f..f54bdf0449c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb @@ -29,27 +29,11 @@ module QA end end - let(:subgroup) do - Resource::Group.fabricate_via_api! do |group| - group.api_client = api_client - group.sandbox = source_group - group.path = "subgroup-for-import-#{SecureRandom.hex(4)}" - end - end - let(:imported_group) do - Resource::Group.init do |group| + Resource::BulkImportGroup.init do |group| group.api_client = api_client group.sandbox = sandbox - group.path = source_group.path - end - end - - let(:imported_subgroup) do - Resource::Group.init do |group| - group.api_client = api_client - group.sandbox = imported_group - group.path = subgroup.path + group.source_group_path = source_group.path end end @@ -61,7 +45,6 @@ module QA # create groups explicitly before connecting gitlab instance source_group - subgroup Flow::Login.sign_in(as: user) Page::Main::Menu.perform(&:go_to_create_group) @@ -78,29 +61,14 @@ module QA 'imports group with subgroups and labels', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785' ) do - Resource::GroupLabel.fabricate_via_api! do |label| - label.api_client = api_client - label.group = source_group - label.title = "source-group-#{SecureRandom.hex(4)}" - end - Resource::GroupLabel.fabricate_via_api! do |label| - label.api_client = api_client - label.group = subgroup - label.title = "subgroup-#{SecureRandom.hex(4)}" - end - Page::Group::BulkImport.perform do |import_page| - import_page.import_group(source_group.path, sandbox.path) - - expect(import_page).to have_imported_group(source_group.path, wait: 300) + import_page.import_group(imported_group.path, imported_group.sandbox.path) - aggregate_failures do - expect { imported_group.reload! }.to eventually_eq(source_group).within(duration: 10) - expect { imported_group.labels }.to eventually_include(*source_group.labels).within(duration: 10) + expect(import_page).to have_imported_group(imported_group.path, wait: 300) - # Do not validate subgroups until https://gitlab.com/gitlab-org/gitlab/-/issues/332818 is resolved - # expect { imported_subgroup.reload! }.to eventually_eq(subgroup).within(duration: 30) - # expect { imported_subgroup.labels }.to eventually_include(*subgroup.labels).within(duration: 30) + imported_group.reload!.visit! + Page::Group::Show.perform do |group| + expect(group).to have_content(imported_group.path) end end end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index a60bb3e6eaf..c6dd56b5f47 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -170,16 +170,16 @@ RSpec.describe QA::Resource::Base do let(:api_resource) { { test: 'api_with_block' } } before do - allow(QA::Runtime::Logger).to receive(:info) + allow(QA::Runtime::Logger).to receive(:debug) end - it 'returns value from api and emits an INFO log entry' do + it 'returns value from api and emits an debug log entry' do result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('api_with_block') expect(QA::Runtime::Logger) - .to have_received(:info).with(/api_with_block/) + .to have_received(:debug).with(/api_with_block/) end end end diff --git a/spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb b/spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb new file mode 100644 index 00000000000..7c10dccdcb9 --- /dev/null +++ b/spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SpammableActions::AkismetMarkAsSpamAction do + include AfterNextHelpers + + controller(ActionController::Base) do + include SpammableActions::AkismetMarkAsSpamAction + + private + + def spammable_path + '/fake_spammable_path' + end + end + + let(:spammable_type) { 'SpammableType' } + let(:spammable) { double(:spammable, spammable_entity_type: double(:spammable_entity_type, titlecase: spammable_type)) } + let(:current_user) { create(:admin) } + + before do + allow(Gitlab::Recaptcha).to receive(:load_configurations!) { true } + routes.draw { get 'mark_as_spam' => 'anonymous#mark_as_spam' } + allow(controller).to receive(:spammable) { spammable } + allow(controller).to receive(:current_user) { double(:current_user, admin?: admin) } + allow(controller).to receive(:current_user).and_return(current_user) + end + + describe '#mark_as_spam' do + subject { post :mark_as_spam } + + before do + expect_next(Spam::AkismetMarkAsSpamService, target: spammable) + .to receive(:execute).and_return(execute_result) + end + + context 'when user is admin', :enable_admin_mode do + let(:admin) { true } + + context 'when service returns truthy' do + let(:execute_result) { true } + + it 'redirects with notice' do + expect(subject).to redirect_to('/fake_spammable_path') + expect(subject.request.flash[:notice]).to match(/#{spammable_type}.*submitted.*successfully/) + end + end + + context 'when service returns falsey' do + let(:execute_result) { false } + + it 'redirects with notice' do + expect(subject).to redirect_to('/fake_spammable_path') + expect(subject.request.flash[:alert]).to match(/Error/) + end + end + end + + context 'when user is not admin' do + let(:admin) { false } + let(:execute_result) { true } + + it 'calls #access_denied!' do + expect(controller).to receive(:access_denied!) { false } + + subject + end + end + end +end diff --git a/spec/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support_spec.rb b/spec/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support_spec.rb new file mode 100644 index 00000000000..1c7d3ab7046 --- /dev/null +++ b/spec/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support_spec.rb @@ -0,0 +1,74 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SpammableActions::CaptchaCheck::HtmlFormatActionsSupport do + controller(ActionController::Base) do + include SpammableActions::CaptchaCheck::HtmlFormatActionsSupport + + def create + with_captcha_check_html_format { render :some_rendered_view } + end + end + + let(:spammable) { double(:spammable) } + + before do + allow(Gitlab::Recaptcha).to receive(:load_configurations!) { true } + routes.draw { get 'create' => 'anonymous#create' } + allow(controller).to receive(:spammable) { spammable } + expect(spammable).to receive(:render_recaptcha?).at_least(:once) { render_recaptcha } + end + + describe '#convert_html_spam_params_to_headers' do + let(:render_recaptcha) { false } + let(:g_recaptcha_response) { 'abc123' } + let(:spam_log_id) { 42 } + + let(:params) do + { + 'g-recaptcha-response' => g_recaptcha_response, + spam_log_id: spam_log_id + } + end + + # NOTE: `:update` has an identical `before_action` behavior to ``:create``, but `before_action` is + # declarative via the ``:only`` attribute, so there's little value in re-testing the behavior. + subject { post :create, params: params } + + before do + allow(controller).to receive(:render).with(:some_rendered_view) + end + + it 'converts params to headers' do + subject + + expect(controller.request.headers['X-GitLab-Captcha-Response']).to eq(g_recaptcha_response) + expect(controller.request.headers['X-GitLab-Spam-Log-Id']).to eq(spam_log_id.to_s) + end + end + + describe '#with_captcha_check_html_format' do + subject { post :create } + + context 'when spammable.render_recaptcha? is true' do + let(:render_recaptcha) { true } + + it 'renders :verify' do + expect(controller).to receive(:render).with(:verify) + + subject + end + end + + context 'when spammable.render_recaptcha? is false' do + let(:render_recaptcha) { false } + + it 'yields to block' do + expect(controller).to receive(:render).with(:some_rendered_view) + + subject + end + end + end +end diff --git a/spec/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support_spec.rb b/spec/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support_spec.rb new file mode 100644 index 00000000000..d7a44351ad8 --- /dev/null +++ b/spec/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe SpammableActions::CaptchaCheck::JsonFormatActionsSupport do + controller(ActionController::Base) do + include SpammableActions::CaptchaCheck::JsonFormatActionsSupport + + def some_action + with_captcha_check_json_format { render :some_rendered_view } + end + end + + before do + allow(Gitlab::Recaptcha).to receive(:load_configurations!) { true } + end + + describe '#with_captcha_check_json_format' do + subject { post :some_action } + + let(:spammable) { double(:spammable) } + + before do + routes.draw { get 'some_action' => 'anonymous#some_action' } + allow(controller).to receive(:spammable) { spammable } + expect(spammable).to receive(:render_recaptcha?).at_least(:once) { render_recaptcha } + end + + context 'when spammable.render_recaptcha? is true' do + let(:render_recaptcha) { true } + let(:spam_log) { double(:spam_log, id: 1) } + let(:spammable) { double(:spammable, spam?: true, render_recaptcha?: render_recaptcha, spam_log: spam_log) } + let(:recaptcha_site_key) { 'abc123' } + let(:spam_action_response_fields) do + { + spam: true, + needs_captcha_response: render_recaptcha, + spam_log_id: 1, + captcha_site_key: recaptcha_site_key + } + end + + it 'renders json containing spam_action_response_fields' do + expect(controller).to receive(:render).with(json: spam_action_response_fields, status: :conflict) + allow(Gitlab::CurrentSettings).to receive(:recaptcha_site_key) { recaptcha_site_key } + subject + end + end + + context 'when spammable.render_recaptcha? is false' do + let(:render_recaptcha) { false } + + it 'yields to block' do + expect(controller).to receive(:render).with(:some_rendered_view) + + subject + end + end + end +end diff --git a/spec/controllers/concerns/spammable_actions_spec.rb b/spec/controllers/concerns/spammable_actions_spec.rb deleted file mode 100644 index 7bd5a76e60c..00000000000 --- a/spec/controllers/concerns/spammable_actions_spec.rb +++ /dev/null @@ -1,112 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe SpammableActions do - controller(ActionController::Base) do - include SpammableActions - - # #update is used here to test #recaptcha_check_with_fallback, but it could be invoked - # from #create or any other action which mutates a spammable via a controller. - def update - should_redirect = params[:should_redirect] == 'true' - - recaptcha_check_with_fallback(should_redirect) { render json: :ok } - end - - private - - def spammable_path - '/fake_spammable_path' - end - end - - before do - allow(Gitlab::Recaptcha).to receive(:load_configurations!) { true } - end - - describe '#recaptcha_check_with_fallback' do - shared_examples 'yields to block' do - it do - subject - - expect(json_response).to eq({ json: 'ok' }) - end - end - - let(:format) { :html } - - subject { post :update, format: format, params: params } - - let(:spammable) { double(:spammable) } - let(:should_redirect) { nil } - let(:params) do - { - should_redirect: should_redirect - } - end - - before do - routes.draw { get 'update' => 'anonymous#update' } - allow(controller).to receive(:spammable) { spammable } - end - - context 'when should_redirect is true and spammable is valid' do - let(:should_redirect) { true } - - before do - allow(spammable).to receive(:valid?) { true } - end - - it 'redirects to spammable_path' do - expect(subject).to redirect_to('/fake_spammable_path') - end - end - - context 'when should_redirect is false or spammable is not valid' do - before do - allow(spammable).to receive(:valid?) { false } - end - - context 'when spammable.render_recaptcha? is true' do - let(:spam_log) { instance_double(SpamLog, id: 123) } - let(:captcha_site_key) { 'abc123' } - - before do - expect(spammable).to receive(:render_recaptcha?).at_least(:once) { true } - end - - context 'when format is :html' do - it 'renders :verify' do - expect(controller).to receive(:render).with(:verify) - - subject - end - end - - context 'when format is :json' do - let(:format) { :json } - - before do - expect(spammable).to receive(:spam?) { false } - expect(spammable).to receive(:spam_log) { spam_log } - expect(Gitlab::CurrentSettings).to receive(:recaptcha_site_key) { captcha_site_key } - end - - it 'renders json with spam_action_response_fields' do - subject - - expected_json_response = HashWithIndifferentAccess.new( - { - spam: false, - needs_captcha_response: true, - spam_log_id: spam_log.id, - captcha_site_key: captcha_site_key - }) - expect(json_response).to eq(expected_json_response) - end - end - end - end - end -end diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb index 922ecb6052a..0c29280316a 100644 --- a/spec/controllers/projects/issues_controller_spec.rb +++ b/spec/controllers/projects/issues_controller_spec.rb @@ -1464,7 +1464,7 @@ RSpec.describe Projects::IssuesController do } end - it 'updates issue' do + it 'updates issue', :enable_admin_mode do post_spam expect(issue.submittable_as_spam?).to be_falsey end diff --git a/spec/controllers/projects/snippets_controller_spec.rb b/spec/controllers/projects/snippets_controller_spec.rb index 291e681dea9..a388fc4620f 100644 --- a/spec/controllers/projects/snippets_controller_spec.rb +++ b/spec/controllers/projects/snippets_controller_spec.rb @@ -110,7 +110,7 @@ RSpec.describe Projects::SnippetsController do } end - it 'updates the snippet' do + it 'updates the snippet', :enable_admin_mode do mark_as_spam expect(snippet.reload).not_to be_submittable_as_spam diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb index 50d6ac8f23d..a82c44fcc44 100644 --- a/spec/controllers/snippets_controller_spec.rb +++ b/spec/controllers/snippets_controller_spec.rb @@ -231,7 +231,7 @@ RSpec.describe SnippetsController do post :mark_as_spam, params: { id: public_snippet.id } end - it 'updates the snippet' do + it 'updates the snippet', :enable_admin_mode do mark_as_spam expect(public_snippet.reload).not_to be_submittable_as_spam diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb index 84d25fe7dbd..faadd24dbf4 100644 --- a/spec/features/boards/boards_spec.rb +++ b/spec/features/boards/boards_spec.rb @@ -12,594 +12,508 @@ RSpec.describe 'Project issue boards', :js do let_it_be(:user) { create(:user) } let_it_be(:user2) { create(:user) } - before do - project.add_maintainer(user) - project.add_maintainer(user2) - - sign_in(user) - - set_cookie('sidebar_collapsed', 'true') - end - - context 'no lists' do + context 'signed in user' do before do - visit_project_board_path_without_query_limit(project, board) - end - - it 'creates default lists' do - lists = %w[Open Closed] + project.add_maintainer(user) + project.add_maintainer(user2) - wait_for_requests + sign_in(user) - expect(page).to have_selector('.board', count: 2) + set_cookie('sidebar_collapsed', 'true') + end - page.all('.board').each_with_index do |list, i| - expect(list.find('.board-title')).to have_content(lists[i]) + context 'no lists' do + before do + visit_project_board_path_without_query_limit(project, board) end - end - end - context 'with lists' do - let_it_be(:milestone) { create(:milestone, project: project) } - - let_it_be(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') } - let_it_be(:development) { create(:label, project: project, name: 'Development') } - let_it_be(:testing) { create(:label, project: project, name: 'Testing') } - let_it_be(:bug) { create(:label, project: project, name: 'Bug') } - let_it_be(:backlog) { create(:label, project: project, name: 'Backlog') } - let_it_be(:closed) { create(:label, project: project, name: 'Closed') } - let_it_be(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } - let_it_be(:a_plus) { create(:label, project: project, name: 'A+') } - let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } - let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } - let_it_be(:backlog_list) { create(:backlog_list, board: board) } - - let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } - let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) } - let_it_be(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) } - let_it_be(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) } - let_it_be(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) } - let_it_be(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) } - let_it_be(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) } - let_it_be(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } - let_it_be(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') } - let_it_be(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } - let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } + it 'creates default lists' do + lists = %w[Open Closed] - before do - stub_feature_flags(board_new_list: false) + wait_for_requests - visit_project_board_path_without_query_limit(project, board) - end + expect(page).to have_selector('.board', count: 2) - it 'shows description tooltip on list title', :quarantine do - page.within('.board:nth-child(2)') do - expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') + page.all('.board').each_with_index do |list, i| + expect(list.find('.board-title')).to have_content(lists[i]) + end end end - it 'shows issues in lists' do - wait_for_board_cards(2, 8) - wait_for_board_cards(3, 2) - end + context 'with lists' do + let_it_be(:milestone) { create(:milestone, project: project) } + + let_it_be(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') } + let_it_be(:development) { create(:label, project: project, name: 'Development') } + let_it_be(:testing) { create(:label, project: project, name: 'Testing') } + let_it_be(:bug) { create(:label, project: project, name: 'Bug') } + let_it_be(:backlog) { create(:label, project: project, name: 'Backlog') } + let_it_be(:closed) { create(:label, project: project, name: 'Closed') } + let_it_be(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') } + let_it_be(:a_plus) { create(:label, project: project, name: 'A+') } + let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } + let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } + let_it_be(:backlog_list) { create(:backlog_list, board: board) } + + let_it_be(:confidential_issue) { create(:labeled_issue, :confidential, project: project, author: user, labels: [planning], relative_position: 9) } + let_it_be(:issue1) { create(:labeled_issue, project: project, title: 'aaa', description: '111', assignees: [user], labels: [planning], relative_position: 8) } + let_it_be(:issue2) { create(:labeled_issue, project: project, title: 'bbb', description: '222', author: user2, labels: [planning], relative_position: 7) } + let_it_be(:issue3) { create(:labeled_issue, project: project, title: 'ccc', description: '333', labels: [planning], relative_position: 6) } + let_it_be(:issue4) { create(:labeled_issue, project: project, title: 'ddd', description: '444', labels: [planning], relative_position: 5) } + let_it_be(:issue5) { create(:labeled_issue, project: project, title: 'eee', description: '555', labels: [planning], milestone: milestone, relative_position: 4) } + let_it_be(:issue6) { create(:labeled_issue, project: project, title: 'fff', description: '666', labels: [planning, development], relative_position: 3) } + let_it_be(:issue7) { create(:labeled_issue, project: project, title: 'ggg', description: '777', labels: [development], relative_position: 2) } + let_it_be(:issue8) { create(:closed_issue, project: project, title: 'hhh', description: '888') } + let_it_be(:issue9) { create(:labeled_issue, project: project, title: 'iii', description: '999', labels: [planning, testing, bug, accepting], relative_position: 1) } + let_it_be(:issue10) { create(:labeled_issue, project: project, title: 'issue +', description: 'A+ great issue', labels: [a_plus]) } - it 'shows confidential issues with icon' do - page.within(find('.board:nth-child(2)')) do - expect(page).to have_selector('.confidential-icon', count: 1) + before do + visit_project_board_path_without_query_limit(project, board) end - end - it 'search closed list' do - find('.filtered-search').set(issue8.title) - find('.filtered-search').native.send_keys(:enter) - - wait_for_requests + it 'shows description tooltip on list title', :quarantine do + page.within('.board:nth-child(2)') do + expect(find('.board-title span.has-tooltip')[:title]).to eq('Test') + end + end - expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 0) - expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 1) - end + it 'shows issues in lists' do + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 2) + end - it 'search list' do - find('.filtered-search').set(issue5.title) - find('.filtered-search').native.send_keys(:enter) + it 'shows confidential issues with icon' do + page.within(find('.board:nth-child(2)')) do + expect(page).to have_selector('.confidential-icon', count: 1) + end + end - wait_for_requests + it 'search closed list' do + find('.filtered-search').set(issue8.title) + find('.filtered-search').native.send_keys(:enter) - expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 1) - expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0) - expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0) - end + wait_for_requests - context 'search list negation queries' do - before do - visit_project_board_path_without_query_limit(project, board) + expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 0) + expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 1) end - it 'does not have the != option' do - find('.filtered-search').set('label:') + it 'search list' do + find('.filtered-search').set(issue5.title) + find('.filtered-search').native.send_keys(:enter) wait_for_requests - within('#js-dropdown-operator') do - tokens = all(:css, 'li.filter-dropdown-item') - expect(tokens.count).to eq(2) - button = tokens[0].find('button') - expect(button).to have_content('=') - button = tokens[1].find('button') - expect(button).to have_content('!=') - end - end - end - it 'allows user to delete board' do - remove_list + expect(find('.board:nth-child(2)')).to have_selector('.board-card', count: 1) + expect(find('.board:nth-child(3)')).to have_selector('.board-card', count: 0) + expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 0) + end - wait_for_requests + context 'search list negation queries' do + before do + visit_project_board_path_without_query_limit(project, board) + end - expect(page).to have_selector('.board', count: 3) - end + it 'does not have the != option' do + find('.filtered-search').set('label:') - it 'infinite scrolls list' do - create_list(:labeled_issue, 30, project: project, labels: [planning]) + wait_for_requests + within('#js-dropdown-operator') do + tokens = all(:css, 'li.filter-dropdown-item') + expect(tokens.count).to eq(2) + button = tokens[0].find('button') + expect(button).to have_content('=') + button = tokens[1].find('button') + expect(button).to have_content('!=') + end + end + end - visit_project_board_path_without_query_limit(project, board) + it 'allows user to delete board' do + remove_list - page.within(find('.board:nth-child(2)')) do - expect(page.find('.board-header')).to have_content('38') - expect(page).to have_selector('.board-card', count: 10) - expect(page).to have_content('Showing 10 of 38 issues') + wait_for_requests - find('.board .board-list') + expect(page).to have_selector('.board', count: 3) + end - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - end + it 'infinite scrolls list' do + create_list(:labeled_issue, 30, project: project, labels: [planning]) - expect(page).to have_selector('.board-card', count: 20) - expect(page).to have_content('Showing 20 of 38 issues') + visit_project_board_path_without_query_limit(project, board) - find('.board .board-list') + page.within(find('.board:nth-child(2)')) do + expect(page.find('.board-header')).to have_content('38') + expect(page).to have_selector('.board-card', count: 10) + expect(page).to have_content('Showing 10 of 38 issues') - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - end + find('.board .board-list') - expect(page).to have_selector('.board-card', count: 30) - expect(page).to have_content('Showing 30 of 38 issues') + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end - find('.board .board-list') + expect(page).to have_selector('.board-card', count: 20) + expect(page).to have_content('Showing 20 of 38 issues') - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - end + find('.board .board-list') - expect(page).to have_selector('.board-card', count: 38) - expect(page).to have_content('Showing all issues') - end - end + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end - context 'closed' do - it 'shows list of closed issues' do - wait_for_board_cards(4, 1) - wait_for_requests - end + expect(page).to have_selector('.board-card', count: 30) + expect(page).to have_content('Showing 30 of 38 issues') - it 'moves issue to closed' do - drag(list_from_index: 1, list_to_index: 3) + find('.board .board-list') - wait_for_board_cards(2, 7) - wait_for_board_cards(3, 2) - wait_for_board_cards(4, 2) + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end - expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 2) - expect(find('.board:nth-child(4)')).to have_content(issue9.title) - expect(find('.board:nth-child(4)')).not_to have_content(planning.title) + expect(page).to have_selector('.board-card', count: 38) + expect(page).to have_content('Showing all issues') + end end - it 'removes all of the same issue to closed' do - drag(list_from_index: 1, list_to_index: 3) + context 'closed' do + it 'shows list of closed issues' do + wait_for_board_cards(4, 1) + wait_for_requests + end - wait_for_board_cards(2, 7) - wait_for_board_cards(3, 2) - wait_for_board_cards(4, 2) + it 'moves issue to closed' do + drag(list_from_index: 1, list_to_index: 3) - expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) - expect(find('.board:nth-child(4)')).to have_content(issue9.title) - expect(find('.board:nth-child(4)')).not_to have_content(planning.title) - end - end + wait_for_board_cards(2, 7) + wait_for_board_cards(3, 2) + wait_for_board_cards(4, 2) - context 'lists' do - it 'changes position of list' do - drag(list_from_index: 2, list_to_index: 1, selector: '.board-header') + expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(4)')).to have_selector('.board-card', count: 2) + expect(find('.board:nth-child(4)')).to have_content(issue9.title) + expect(find('.board:nth-child(4)')).not_to have_content(planning.title) + end - expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) - expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) + it 'removes all of the same issue to closed' do + drag(list_from_index: 1, list_to_index: 3) - # Make sure list positions are preserved after a reload - visit_project_board_path_without_query_limit(project, board) + wait_for_board_cards(2, 7) + wait_for_board_cards(3, 2) + wait_for_board_cards(4, 2) - expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) - expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) + expect(find('.board:nth-child(2)')).not_to have_content(issue9.title) + expect(find('.board:nth-child(4)')).to have_content(issue9.title) + expect(find('.board:nth-child(4)')).not_to have_content(planning.title) + end end - context 'without backlog and closed lists' do - let_it_be(:board) { create(:board, project: project, hide_backlog_list: true, hide_closed_list: true) } - let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } - let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } - + context 'lists' do it 'changes position of list' do - visit_project_board_path_without_query_limit(project, board) - - drag(list_from_index: 0, list_to_index: 1, selector: '.board-header') + drag(list_from_index: 2, list_to_index: 1, selector: '.board-header') - expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) - expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) # Make sure list positions are preserved after a reload visit_project_board_path_without_query_limit(project, board) - expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) - expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(3) [data-testid="board-list-header"]')).to have_content(planning.title) end - end - it 'dragging does not duplicate list' do - selector = '.board:not(.is-ghost) .board-header' - expect(page).to have_selector(selector, text: development.title, count: 1) + context 'without backlog and closed lists' do + let_it_be(:board) { create(:board, project: project, hide_backlog_list: true, hide_closed_list: true) } + let_it_be(:list1) { create(:list, board: board, label: planning, position: 0) } + let_it_be(:list2) { create(:list, board: board, label: development, position: 1) } - drag(list_from_index: 2, list_to_index: 1, selector: '.board-header', perform_drop: false) + it 'changes position of list' do + visit_project_board_path_without_query_limit(project, board) - expect(page).to have_selector(selector, text: development.title, count: 1) - end + drag(list_from_index: 0, list_to_index: 1, selector: '.board-header') - it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do - drag(list_from_index: 1, from_index: 1, list_to_index: 2) + expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) - wait_for_board_cards(2, 7) - wait_for_board_cards(3, 2) - wait_for_board_cards(4, 1) - - expect(find('.board:nth-child(3)')).to have_content(issue6.title) - expect(find('.board:nth-child(3)').all('.board-card').last).not_to have_content(development.title) - end + # Make sure list positions are preserved after a reload + visit_project_board_path_without_query_limit(project, board) - it 'issue moves between lists and does not show the "Planning" label since the card is in the "Planning" list label' do - drag(list_from_index: 2, list_to_index: 1) + expect(find('.board:nth-child(1) [data-testid="board-list-header"]')).to have_content(development.title) + expect(find('.board:nth-child(2) [data-testid="board-list-header"]')).to have_content(planning.title) + end + end - wait_for_board_cards(2, 9) - wait_for_board_cards(3, 1) - wait_for_board_cards(4, 1) + it 'dragging does not duplicate list' do + selector = '.board:not(.is-ghost) .board-header' + expect(page).to have_selector(selector, text: development.title, count: 1) - expect(find('.board:nth-child(2)')).to have_content(issue7.title) - expect(find('.board:nth-child(2)').all('.board-card').first).not_to have_content(planning.title) - end + drag(list_from_index: 2, list_to_index: 1, selector: '.board-header', perform_drop: false) - it 'issue moves from closed' do - drag(list_from_index: 2, list_to_index: 3) + expect(page).to have_selector(selector, text: development.title, count: 1) + end - wait_for_board_cards(2, 8) - wait_for_board_cards(3, 1) - wait_for_board_cards(4, 2) + it 'issue moves between lists and does not show the "Development" label since the card is in the "Development" list label' do + drag(list_from_index: 1, from_index: 1, list_to_index: 2) - expect(find('.board:nth-child(4)')).to have_content(issue8.title) - end + wait_for_board_cards(2, 7) + wait_for_board_cards(3, 2) + wait_for_board_cards(4, 1) - context 'issue card' do - it 'shows assignee' do - page.within(find('.board:nth-child(2)')) do - expect(page).to have_selector('.avatar', count: 1) - end + expect(find('.board:nth-child(3)')).to have_content(issue6.title) + expect(find('.board:nth-child(3)').all('.board-card').last).not_to have_content(development.title) end - context 'list header' do - let(:total_planning_issues) { "8" } + it 'issue moves between lists and does not show the "Planning" label since the card is in the "Planning" list label' do + drag(list_from_index: 2, list_to_index: 1) - it 'shows issue count on the list' do - page.within(find(".board:nth-child(2)")) do - expect(page.find('[data-testid="board-items-count"]')).to have_text(total_planning_issues) - expect(page).not_to have_selector('.js-max-issue-size') - end - end + wait_for_board_cards(2, 9) + wait_for_board_cards(3, 1) + wait_for_board_cards(4, 1) + + expect(find('.board:nth-child(2)')).to have_content(issue7.title) + expect(find('.board:nth-child(2)').all('.board-card').first).not_to have_content(planning.title) end - end - context 'new list' do - it 'shows all labels in new list dropdown' do - click_button 'Add list' + it 'issue moves from closed' do + drag(list_from_index: 2, list_to_index: 3) - wait_for_requests + wait_for_board_cards(2, 8) + wait_for_board_cards(3, 1) + wait_for_board_cards(4, 2) - page.within('.dropdown-menu-issues-board-new') do - expect(page).to have_content(planning.title) - expect(page).to have_content(development.title) - expect(page).to have_content(testing.title) - end + expect(find('.board:nth-child(4)')).to have_content(issue8.title) end - it 'creates new list for label' do - click_button 'Add list' - wait_for_requests - - page.within('.dropdown-menu-issues-board-new') do - click_link testing.title + context 'issue card' do + it 'shows assignee' do + page.within(find('.board:nth-child(2)')) do + expect(page).to have_selector('.avatar', count: 1) + end end - wait_for_requests + context 'list header' do + let(:total_planning_issues) { "8" } - expect(page).to have_selector('.board', count: 5) + it 'shows issue count on the list' do + page.within(find(".board:nth-child(2)")) do + expect(page.find('[data-testid="board-items-count"]')).to have_text(total_planning_issues) + expect(page).not_to have_selector('.js-max-issue-size') + end + end + end end + end - it 'creates new list for Backlog label' do - click_button 'Add list' - wait_for_requests - - page.within('.dropdown-menu-issues-board-new') do - click_link backlog.title - end + context 'filtering' do + it 'filters by author' do + set_filter("author", user2.username) + click_filter_link(user2.username) + submit_filter wait_for_requests - - expect(page).to have_selector('.board', count: 5) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end - it 'creates new list for Closed label' do - click_button 'Add list' - wait_for_requests - - page.within('.dropdown-menu-issues-board-new') do - click_link closed.title - end + it 'filters by assignee' do + set_filter("assignee", user.username) + click_filter_link(user.username) + submit_filter wait_for_requests - expect(page).to have_selector('.board', count: 5) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end - it 'keeps dropdown open after adding new list' do - click_button 'Add list' - wait_for_requests - - page.within('.dropdown-menu-issues-board-new') do - click_link closed.title - end + it 'filters by milestone' do + set_filter("milestone", "\"#{milestone.title}") + click_filter_link(milestone.title) + submit_filter wait_for_requests - - expect(page).to have_css('#js-add-list.show') + wait_for_board_cards(2, 1) + wait_for_board_cards(3, 0) + wait_for_board_cards(4, 0) end - it 'creates new list from a new label' do - click_button 'Add list' + it 'filters by label' do + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter wait_for_requests + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) + end - click_link 'Create project label' + it 'filters by label with encoded character' do + set_filter("label", a_plus.title) + click_filter_link(a_plus.title) + submit_filter - fill_in('new_label_name', with: 'Testing New Label - with list') + wait_for_board_cards(1, 1) + wait_for_empty_boards((2..4)) + end - first('.suggest-colors a').click + it 'filters by label with space after reload', :quarantine do + set_filter("label", "\"#{accepting.title}") + click_filter_link(accepting.title) + submit_filter - click_button 'Create' + # Test after reload + page.evaluate_script 'window.location.reload()' + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) wait_for_requests - wait_for_requests - expect(page).to have_selector('.board', count: 5) + page.within(find('.board:nth-child(2)')) do + expect(page.find('.board-header')).to have_content('1') + expect(page).to have_selector('.board-card', count: 1) + end + + page.within(find('.board:nth-child(3)')) do + expect(page.find('.board-header')).to have_content('0') + expect(page).to have_selector('.board-card', count: 0) + end end - end - end - context 'filtering' do - it 'filters by author' do - set_filter("author", user2.username) - click_filter_link(user2.username) - submit_filter + it 'removes filtered labels' do + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + set_filter("label", testing.title) + click_filter_link(testing.title) + submit_filter - wait_for_requests - wait_for_board_cards(2, 1) - wait_for_empty_boards((3..4)) - end + wait_for_board_cards(2, 1) - it 'filters by assignee' do - set_filter("assignee", user.username) - click_filter_link(user.username) - submit_filter + find('.clear-search').click + submit_filter + end - wait_for_requests + wait_for_board_cards(2, 8) + end - wait_for_board_cards(2, 1) - wait_for_empty_boards((3..4)) - end + it 'infinite scrolls list with label filter' do + create_list(:labeled_issue, 30, project: project, labels: [planning, testing]) - it 'filters by milestone' do - set_filter("milestone", "\"#{milestone.title}") - click_filter_link(milestone.title) - submit_filter + set_filter("label", testing.title) + click_filter_link(testing.title) + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + submit_filter + end - wait_for_requests - wait_for_board_cards(2, 1) - wait_for_board_cards(3, 0) - wait_for_board_cards(4, 0) - end + wait_for_requests - it 'filters by label' do - set_filter("label", testing.title) - click_filter_link(testing.title) - submit_filter + page.within(find('.board:nth-child(2)')) do + expect(page.find('.board-header')).to have_content('31') + expect(page).to have_selector('.board-card', count: 10) + expect(page).to have_content('Showing 10 of 31 issues') - wait_for_requests - wait_for_board_cards(2, 1) - wait_for_empty_boards((3..4)) - end + find('.board .board-list') - it 'filters by label with encoded character' do - set_filter("label", a_plus.title) - click_filter_link(a_plus.title) - submit_filter + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end - wait_for_board_cards(1, 1) - wait_for_empty_boards((2..4)) - end + expect(page).to have_selector('.board-card', count: 20) + expect(page).to have_content('Showing 20 of 31 issues') - it 'filters by label with space after reload', :quarantine do - set_filter("label", "\"#{accepting.title}") - click_filter_link(accepting.title) - submit_filter + find('.board .board-list') - # Test after reload - page.evaluate_script 'window.location.reload()' - wait_for_board_cards(2, 1) - wait_for_empty_boards((3..4)) + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end - wait_for_requests + expect(page).to have_selector('.board-card', count: 30) + expect(page).to have_content('Showing 30 of 31 issues') - page.within(find('.board:nth-child(2)')) do - expect(page.find('.board-header')).to have_content('1') - expect(page).to have_selector('.board-card', count: 1) - end + find('.board .board-list') + inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + end - page.within(find('.board:nth-child(3)')) do - expect(page.find('.board-header')).to have_content('0') - expect(page).to have_selector('.board-card', count: 0) + expect(page).to have_selector('.board-card', count: 31) + expect(page).to have_content('Showing all issues') + end end - end - it 'removes filtered labels' do - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do + it 'filters by multiple labels', :quarantine do set_filter("label", testing.title) click_filter_link(testing.title) - submit_filter - wait_for_board_cards(2, 1) + set_filter("label", bug.title) + click_filter_link(bug.title) - find('.clear-search').click submit_filter - end - - wait_for_board_cards(2, 8) - end - it 'infinite scrolls list with label filter' do - create_list(:labeled_issue, 30, project: project, labels: [planning, testing]) + wait_for_requests - set_filter("label", testing.title) - click_filter_link(testing.title) - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - submit_filter + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end - wait_for_requests - - page.within(find('.board:nth-child(2)')) do - expect(page.find('.board-header')).to have_content('31') - expect(page).to have_selector('.board-card', count: 10) - expect(page).to have_content('Showing 10 of 31 issues') - - find('.board .board-list') - - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + it 'filters by clicking label button on issue' do + page.within(find('.board:nth-child(2)')) do + expect(page).to have_selector('.board-card', count: 8) + expect(find('.board-card', match: :first)).to have_content(bug.title) + click_link(bug.title) + wait_for_requests end - expect(page).to have_selector('.board-card', count: 20) - expect(page).to have_content('Showing 20 of 31 issues') - - find('.board .board-list') - - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") + page.within('.tokens-container') do + expect(page).to have_content(bug.title) end - expect(page).to have_selector('.board-card', count: 30) - expect(page).to have_content('Showing 30 of 31 issues') - - find('.board .board-list') - inspect_requests(inject_headers: { 'X-GITLAB-DISABLE-SQL-QUERY-LIMIT' => 'https://gitlab.com/gitlab-org/gitlab/-/issues/323426' }) do - evaluate_script("document.querySelectorAll('.board .board-list')[1].scrollTop = document.querySelectorAll('.board .board-list')[1].scrollHeight") - end - - expect(page).to have_selector('.board-card', count: 31) - expect(page).to have_content('Showing all issues') - end - end - - it 'filters by multiple labels', :quarantine do - set_filter("label", testing.title) - click_filter_link(testing.title) - - set_filter("label", bug.title) - click_filter_link(bug.title) - - submit_filter - - wait_for_requests - - wait_for_board_cards(2, 1) - wait_for_empty_boards((3..4)) - end - - it 'filters by clicking label button on issue' do - page.within(find('.board:nth-child(2)')) do - expect(page).to have_selector('.board-card', count: 8) - expect(find('.board-card', match: :first)).to have_content(bug.title) - click_link(bug.title) wait_for_requests - end - page.within('.tokens-container') do - expect(page).to have_content(bug.title) + wait_for_board_cards(2, 1) + wait_for_empty_boards((3..4)) end - wait_for_requests + it 'removes label filter by clicking label button on issue' do + page.within(find('.board:nth-child(2)')) do + page.within(find('.board-card', match: :first)) do + click_link(bug.title) + end - wait_for_board_cards(2, 1) - wait_for_empty_boards((3..4)) - end + wait_for_requests - it 'removes label filter by clicking label button on issue' do - page.within(find('.board:nth-child(2)')) do - page.within(find('.board-card', match: :first)) do - click_link(bug.title) + expect(page).to have_selector('.board-card', count: 1) end wait_for_requests - - expect(page).to have_selector('.board-card', count: 1) end - - wait_for_requests end end - end - context 'issue board focus mode' do - before do - visit project_board_path(project, board) - wait_for_requests - end + context 'issue board focus mode' do + before do + visit project_board_path(project, board) + wait_for_requests + end - it 'shows the button' do - expect(page).to have_button('Toggle focus mode') + it 'shows the button' do + expect(page).to have_button('Toggle focus mode') + end end - end - context 'keyboard shortcuts' do - before do - visit_project_board_path_without_query_limit(project, board) - wait_for_requests - end + context 'keyboard shortcuts' do + before do + visit_project_board_path_without_query_limit(project, board) + wait_for_requests + end - it 'allows user to use keyboard shortcuts' do - find('body').native.send_keys('i') - expect(page).to have_content('New Issue') + it 'allows user to use keyboard shortcuts' do + find('body').native.send_keys('i') + expect(page).to have_content('New Issue') + end end end context 'signed out user' do before do - sign_out(:user) visit project_board_path(project, board) wait_for_requests end @@ -609,7 +523,7 @@ RSpec.describe 'Project issue boards', :js do end it 'does not show create new list' do - expect(page).not_to have_button('.js-new-board-list') + expect(page).not_to have_button('Create list') end it 'does not allow dragging' do @@ -622,14 +536,13 @@ RSpec.describe 'Project issue boards', :js do before do project.add_guest(user_guest) - sign_out(:user) sign_in(user_guest) visit project_board_path(project, board) wait_for_requests end it 'does not show create new list' do - expect(page).not_to have_selector('.js-new-board-list') + expect(page).not_to have_button('Create list') end end diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js index 555767dd549..7d3ecc773a6 100644 --- a/spec/frontend/boards/board_card_inner_spec.js +++ b/spec/frontend/boards/board_card_inner_spec.js @@ -9,7 +9,7 @@ import { issuableTypes } from '~/boards/constants'; import eventHub from '~/boards/eventhub'; import defaultStore from '~/boards/stores'; import { updateHistory } from '~/lib/utils/url_utility'; -import { mockLabelList, mockIssue } from './mock_data'; +import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data'; jest.mock('~/lib/utils/url_utility'); jest.mock('~/boards/eventhub'); @@ -45,7 +45,7 @@ describe('Board card component', () => { const findEpicCountablesTotalWeight = () => wrapper.findByTestId('epic-countables-total-weight'); const findEpicProgressTooltip = () => wrapper.findByTestId('epic-progress-tooltip-content'); - const createStore = ({ isEpicBoard = false } = {}) => { + const createStore = ({ isEpicBoard = false, isProjectBoard = false } = {}) => { store = new Vuex.Store({ ...defaultStore, state: { @@ -55,7 +55,7 @@ describe('Board card component', () => { getters: { isGroupBoard: () => true, isEpicBoard: () => isEpicBoard, - isProjectBoard: () => false, + isProjectBoard: () => isProjectBoard, }, }); }; @@ -134,6 +134,17 @@ describe('Board card component', () => { expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false); }); + it('does not render item reference path', () => { + createStore({ isProjectBoard: true }); + createWrapper(); + + expect(wrapper.find('.board-card-number').text()).not.toContain(mockIssueFullPath); + }); + + it('renders item reference path', () => { + expect(wrapper.find('.board-card-number').text()).toContain(mockIssueFullPath); + }); + describe('blocked', () => { it('renders blocked icon if issue is blocked', async () => { createWrapper({ diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js index 9a9ce7b8dc1..25ec568e48d 100644 --- a/spec/frontend/boards/components/board_card_spec.js +++ b/spec/frontend/boards/components/board_card_spec.js @@ -31,6 +31,7 @@ describe('Board card', () => { actions: mockActions, getters: { isEpicBoard: () => false, + isProjectBoard: () => false, }, }); }; diff --git a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb index 43d846cb297..77fd6cddc09 100644 --- a/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb +++ b/spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb @@ -58,7 +58,7 @@ RSpec.describe 'Mark snippet as spam' do end it 'marks snippet as spam' do - expect_next(Spam::MarkAsSpamService, target: snippet) + expect_next(Spam::AkismetMarkAsSpamService, target: snippet) .to receive(:execute).and_return(true) post_graphql_mutation(mutation, current_user: current_user) diff --git a/spec/services/spam/mark_as_spam_service_spec.rb b/spec/services/spam/akismet_mark_as_spam_service_spec.rb index 308a66c3a48..12666e23e47 100644 --- a/spec/services/spam/mark_as_spam_service_spec.rb +++ b/spec/services/spam/akismet_mark_as_spam_service_spec.rb @@ -2,7 +2,7 @@ require 'spec_helper' -RSpec.describe Spam::MarkAsSpamService do +RSpec.describe Spam::AkismetMarkAsSpamService do let(:user_agent_detail) { build(:user_agent_detail) } let(:spammable) { build(:issue, user_agent_detail: user_agent_detail) } let(:fake_akismet_service) { double(:akismet_service, submit_spam: true) } |