Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--app/assets/javascripts/boards/components/board_card_inner.vue7
-rw-r--r--app/controllers/concerns/issuable_actions.rb50
-rw-r--r--app/controllers/concerns/spammable_actions.rb73
-rw-r--r--app/controllers/concerns/spammable_actions/akismet_mark_as_spam_action.rb28
-rw-r--r--app/controllers/concerns/spammable_actions/attributes.rb13
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/common.rb23
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support.rb35
-rw-r--r--app/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support.rb25
-rw-r--r--app/controllers/projects/issues_controller.rb11
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/graphql/mutations/snippets/mark_as_spam.rb2
-rw-r--r--app/services/spam/akismet_mark_as_spam_service.rb (renamed from app/services/spam/mark_as_spam_service.rb)6
-rw-r--r--doc/api/events.md2
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/resource/base.rb2
-rw-r--r--qa/qa/resource/bulk_import_group.rb76
-rw-r--r--qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb104
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb46
-rw-r--r--qa/spec/resource/base_spec.rb6
-rw-r--r--spec/controllers/concerns/spammable_actions/akismet_mark_as_spam_action_spec.rb71
-rw-r--r--spec/controllers/concerns/spammable_actions/captcha_check/html_format_actions_support_spec.rb74
-rw-r--r--spec/controllers/concerns/spammable_actions/captcha_check/json_format_actions_support_spec.rb60
-rw-r--r--spec/controllers/concerns/spammable_actions_spec.rb112
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/snippets_controller_spec.rb2
-rw-r--r--spec/controllers/snippets_controller_spec.rb2
-rw-r--r--spec/features/boards/boards_spec.rb771
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js17
-rw-r--r--spec/frontend/boards/components/board_card_spec.js1
-rw-r--r--spec/requests/api/graphql/mutations/snippets/mark_as_spam_spec.rb2
-rw-r--r--spec/services/spam/akismet_mark_as_spam_service_spec.rb (renamed from spec/services/spam/mark_as_spam_service_spec.rb)2
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
diff --git a/qa/qa.rb b/qa/qa.rb
index d560dc50174..e17b2b86ceb 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -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) }