diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-10 03:10:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-06-10 03:10:09 +0300 |
commit | 26774b8d98581b2f211cd5cf04dcec0e352c04a6 (patch) | |
tree | fe4277340df965a25b73012973016fd26006457f | |
parent | f820d18e56f2bd63dd0f91b076ace59345a036a1 (diff) |
Add latest changes from gitlab-org/gitlab@master
28 files changed, 518 insertions, 523 deletions
diff --git a/app/assets/javascripts/admin/users/components/users_table.vue b/app/assets/javascripts/admin/users/components/users_table.vue index ede5c26e487..c848908e024 100644 --- a/app/assets/javascripts/admin/users/components/users_table.vue +++ b/app/assets/javascripts/admin/users/components/users_table.vue @@ -107,6 +107,7 @@ export default { :items="users" :fields="$options.fields" :empty-text="s__('AdminUsers|No users found')" + data-qa-selector="user_row_content" show-empty stacked="md" :tbody-tr-attr="{ 'data-qa-selector': 'user_row_content' }" diff --git a/app/finders/deployments_finder.rb b/app/finders/deployments_finder.rb index acce038dba6..bb9d204ab73 100644 --- a/app/finders/deployments_finder.rb +++ b/app/finders/deployments_finder.rb @@ -136,7 +136,7 @@ class DeploymentsFinder # Implicitly enforce the ordering when filtered by `updated_at` column for performance optimization. # See https://gitlab.com/gitlab-org/gitlab/-/issues/325627#note_552417509. # We remove this in https://gitlab.com/gitlab-org/gitlab/-/issues/328500. - if filter_by_updated_at? && implicitly_enforce_ordering_for_updated_at_filter? + if filter_by_updated_at? sort_params.replace('updated_at' => sort_direction) end @@ -170,15 +170,6 @@ class DeploymentsFinder params[:order_by].to_s == 'finished_at' end - def implicitly_enforce_ordering_for_updated_at_filter? - return false unless params[:project].present? - - ::Feature.enabled?( - :deployments_finder_implicitly_enforce_ordering_for_updated_at_filter, - params[:project], - default_enabled: :yaml) - end - # rubocop: disable CodeReuse/ActiveRecord def preload_associations(scope) scope.includes( diff --git a/app/models/key.rb b/app/models/key.rb index b8b9bd53a63..64385953865 100644 --- a/app/models/key.rb +++ b/app/models/key.rb @@ -7,7 +7,7 @@ class Key < ApplicationRecord include Sortable include Sha256Attribute include Expirable - include EachBatch + include FromUnion sha256_attribute :fingerprint_sha256 diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml deleted file mode 100644 index 2816a1061b9..00000000000 --- a/app/views/admin/users/_user.html.haml +++ /dev/null @@ -1,72 +0,0 @@ -.gl-responsive-table-row{ role: 'row', data: { qa_selector: 'user_row_content' } } - .table-section.section-40 - .table-mobile-header{ role: 'rowheader' } - = _('Name') - .table-mobile-content - = render 'user_detail', user: user - .table-section.section-10 - .table-mobile-header{ role: 'rowheader' } - = _('Projects') - .table-mobile-content.gl-str-truncated{ data: { testid: "user-project-count-#{user.id}" } } - = user.authorized_projects.length - .table-section.section-15 - .table-mobile-header{ role: 'rowheader' } - = _('Created on') - .table-mobile-content - = l(user.created_at.to_date, format: :admin) - .table-section.section-15 - .table-mobile-header{ role: 'rowheader' } - = _('Last activity') - .table-mobile-content - = user.last_activity_on.nil? ? _('Never') : l(user.last_activity_on, format: :admin) - - unless user.internal? - .table-section.section-20.table-button-footer - .table-action-buttons{ data: { testid: "user-actions-#{user.id}" } } - = link_to _('Edit'), edit_admin_user_path(user), class: 'btn gl-button btn-default' - - unless user == current_user - %button.dropdown-new.btn.gl-button.btn-default{ type: 'button', data: { testid: "dropdown-toggle", toggle: 'dropdown' } } - = sprite_icon('settings') - = sprite_icon('chevron-down') - %ul.dropdown-menu.dropdown-menu-right - %li.dropdown-header - = _('Settings') - %li - - if user.ldap_blocked? - %span.small - = s_('AdminUsers|Cannot unblock LDAP blocked users') - - elsif user.blocked? - - if user.blocked_pending_approval? - = link_to s_('AdminUsers|Approve'), approve_admin_user_path(user), method: :put - = link_to s_('AdminUsers|Reject'), reject_admin_user_path(user), method: :delete - - else - %button.gl-button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_unblock_data(user) } - = s_('AdminUsers|Unblock') - - else - %button.gl-button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_block_data(user, user_block_effects) } - = s_('AdminUsers|Block') - - if user.can_be_deactivated? - %li - %button.gl-button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_deactivation_data(user, user_deactivation_effects) } - = s_('AdminUsers|Deactivate') - - elsif user.deactivated? - %li - %button.gl-button.btn.btn-default-tertiary.js-confirm-modal-button{ data: user_activation_data(user) } - = s_('AdminUsers|Activate') - - if user.access_locked? - %li - = link_to _('Unlock'), unlock_admin_user_path(user), method: :put, data: { confirm: _('Are you sure?') } - - if can?(current_user, :destroy_user, user) && !user.blocked_pending_approval? - %li.divider - - if user.can_be_removed? - %li - %button.js-delete-user-modal-button.gl-button.btn.btn-danger-tertiary{ data: { 'gl-modal-action': 'delete', - delete_user_url: admin_user_path(user), - block_user_url: block_admin_user_path(user), - username: sanitize_name(user.name) } } - = s_('AdminUsers|Delete user') - %li - %button.js-delete-user-modal-button.gl-button.btn.btn-danger-tertiary{ data: { 'gl-modal-action': 'delete-with-contributions', - delete_user_url: admin_user_path(user, hard_delete: true), - block_user_url: block_admin_user_path(user), - username: sanitize_name(user.name) } } - = s_('AdminUsers|Delete user and contributions') diff --git a/app/views/admin/users/_users.html.haml b/app/views/admin/users/_users.html.haml index e4438f38a47..1a43d91b800 100644 --- a/app/views/admin/users/_users.html.haml +++ b/app/views/admin/users/_users.html.haml @@ -73,20 +73,9 @@ = link_to admin_users_path(sort: value, filter: params[:filter], search_query: params[:search_query]) do = title -- if Feature.enabled?(:vue_admin_users, default_enabled: :yaml) - #js-admin-users-app{ data: admin_users_data_attributes(@users) } - .gl-spinner-container.gl-my-7 - %span.gl-vertical-align-bottom.gl-spinner.gl-spinner-dark.gl-spinner-lg{ aria: { label: _('Loading') } } -- elsif @users.empty? - .nothing-here-block.border-top-0 - = s_('AdminUsers|No users found') -- else - .table-holder - .thead-white.text-nowrap.gl-responsive-table-row.table-row-header{ role: 'row' } - - user_table_headers.each do |header| - .table-section{ class: header[:section_class_name], role: 'rowheader' }= header[:header_text] - - = render partial: 'admin/users/user', collection: @users +#js-admin-users-app{ data: admin_users_data_attributes(@users) } + .gl-spinner-container.gl-my-7 + %span.gl-vertical-align-bottom.gl-spinner.gl-spinner-dark.gl-spinner-lg{ aria: { label: _('Loading') } } = paginate_collection @users diff --git a/app/workers/ssh_keys/expired_notification_worker.rb b/app/workers/ssh_keys/expired_notification_worker.rb index 95c9601a04c..e8baf0c28dd 100644 --- a/app/workers/ssh_keys/expired_notification_worker.rb +++ b/app/workers/ssh_keys/expired_notification_worker.rb @@ -11,12 +11,31 @@ module SshKeys tags :exclude_from_kubernetes idempotent! + BATCH_SIZE = 500 + + # rubocop: disable CodeReuse/ActiveRecord def perform return unless ::Feature.enabled?(:ssh_key_expiration_email_notification, default_enabled: :yaml) - # rubocop:disable CodeReuse/ActiveRecord - Key.expired_and_not_notified.each_batch(of: 1000) do |relation| # rubocop:disable Cop/InBatches - users = User.where(id: relation.select(:user_id)) + order = Gitlab::Pagination::Keyset::Order.build([ + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'expires_at_utc', + order_expression: Arel.sql("date(expires_at AT TIME ZONE 'UTC')").asc, + nullable: :not_nullable, + distinct: false, + add_to_projections: true + ), + Gitlab::Pagination::Keyset::ColumnOrderDefinition.new( + attribute_name: 'id', + order_expression: Key.arel_table[:id].asc + ) + ]) + + scope = Key.expired_and_not_notified.order(order) + + iterator = Gitlab::Pagination::Keyset::Iterator.new(scope: scope, use_union_optimization: true) + iterator.each_batch(of: BATCH_SIZE) do |relation| + users = User.where(id: relation.map(&:user_id)) # Keyset pagination will load the rows users.each do |user| with_context(user: user) do @@ -24,7 +43,7 @@ module SshKeys end end end - # rubocop:enable CodeReuse/ActiveRecord end + # rubocop: enable CodeReuse/ActiveRecord end end diff --git a/config/feature_flags/development/deployments_finder_implicitly_enforce_ordering_for_updated_at_filter.yml b/config/feature_flags/development/deployments_finder_implicitly_enforce_ordering_for_updated_at_filter.yml deleted file mode 100644 index 68a3de44e88..00000000000 --- a/config/feature_flags/development/deployments_finder_implicitly_enforce_ordering_for_updated_at_filter.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: deployments_finder_implicitly_enforce_ordering_for_updated_at_filter -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59771 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329286 -milestone: '13.12' -type: development -group: group::release -default_enabled: true diff --git a/config/feature_flags/development/vue_admin_users.yml b/config/feature_flags/development/vue_admin_users.yml deleted file mode 100644 index d7a37542cfd..00000000000 --- a/config/feature_flags/development/vue_admin_users.yml +++ /dev/null @@ -1,8 +0,0 @@ ---- -name: vue_admin_users -introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48922 -rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/290737 -milestone: '13.7' -type: development -group: group::compliance -default_enabled: true diff --git a/db/migrate/20210609090856_add_expiry_id_ssh_key_notification_index.rb b/db/migrate/20210609090856_add_expiry_id_ssh_key_notification_index.rb new file mode 100644 index 00000000000..406bbe2095f --- /dev/null +++ b/db/migrate/20210609090856_add_expiry_id_ssh_key_notification_index.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +class AddExpiryIdSshKeyNotificationIndex < ActiveRecord::Migration[6.1] + include Gitlab::Database::MigrationHelpers + + disable_ddl_transaction! + + INDEX_NAME = 'index_keys_on_expires_at_and_id' + + def up + add_concurrent_index :keys, + "date(timezone('UTC', expires_at)), id", + where: 'expiry_notification_delivered_at IS NULL', + name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :keys, INDEX_NAME + end +end diff --git a/db/schema_migrations/20210609090856 b/db/schema_migrations/20210609090856 new file mode 100644 index 00000000000..f0c3c25a01b --- /dev/null +++ b/db/schema_migrations/20210609090856 @@ -0,0 +1 @@ +597e04c51bdad1900b2535c9d664c9e3a4d2a5879e657ef470bbc7ac461d3cca
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5fa8419bd40..8a3da9ceed9 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -23615,6 +23615,8 @@ CREATE INDEX index_jira_tracker_data_on_service_id ON jira_tracker_data USING bt CREATE INDEX index_keys_on_expires_at_and_expiry_notification_undelivered ON keys USING btree (date(timezone('UTC'::text, expires_at)), expiry_notification_delivered_at) WHERE (expiry_notification_delivered_at IS NULL); +CREATE INDEX index_keys_on_expires_at_and_id ON keys USING btree (date(timezone('UTC'::text, expires_at)), id) WHERE (expiry_notification_delivered_at IS NULL); + CREATE UNIQUE INDEX index_keys_on_fingerprint ON keys USING btree (fingerprint); CREATE INDEX index_keys_on_fingerprint_sha256 ON keys USING btree (fingerprint_sha256); diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 74e9eca443a..e75751a36bc 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1544,15 +1544,6 @@ elements: |:------------|:--------------------------------|:----------------------| | _go to_ | making a browser go to location | "navigate to", "open" | -### Permissions vs roles - -GitLab users are [assigned roles](../../../user/permissions.md) that confer specific permissions: - -- _Maintainer_ is an example of a role. -- _Create new issue_ is an example of a permission. - -[Do not use](word_list.md) these terms interchangeably. - ## GitLab versions GitLab product documentation pages (not including [Contributor and Development](../../README.md) diff --git a/doc/development/documentation/styleguide/word_list.md b/doc/development/documentation/styleguide/word_list.md index ce10b11e90a..fd8766bbfb6 100644 --- a/doc/development/documentation/styleguide/word_list.md +++ b/doc/development/documentation/styleguide/word_list.md @@ -36,6 +36,13 @@ Try to avoid extra words when referring to an example or table in a documentatio Do not use when talking about the product or its features. The documentation describes the product as it is today. ([Vale](../testing.md#vale) rule: [`CurrentStatus.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/CurrentStatus.yml)) +## Developer + +When writing about the Developer role, use a capital "D." Do not use the phrase, "if you are a developer" +to mean someone who is assigned the Developer role. Instead, write it out. "If you are assigned the Developer role..." + +Do not use "Developer permissions." A user who is assigned the Developer role has a set of associated permissions. + ## easily Do not use. If the user doesn't find the process to be these things, we lose their trust. @@ -52,6 +59,13 @@ When possible, use present tense instead. For example, use `after you execute th Do not make possessive (GitLab's). This guidance follows [GitLab Brand Guidelines](https://about.gitlab.com/handbook/marketing/corporate-marketing/brand-activation/brand-guidelines/#trademark). +## Guest + +When writing about the Guest role, use a capital "G." Do not use the phrase, "if you are a guest" +to mean someone who is assigned the Guest role. Instead, write it out. "If you are assigned the Guest role..." + +Do not use "Guest permissions." A user who is assigned the Guest role has a set of associated permissions. + ## handy Do not use. If the user doesn't find the process to be these things, we lose their trust. @@ -68,6 +82,13 @@ Do not use first-person singular. Use **you**, **we**, or **us** instead. ([Vale Do not use Latin abbreviations. Use **that is** instead. ([Vale](../testing.md#vale) rule: [`LatinTerms.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/LatinTerms.yml)) +## Maintainer + +When writing about the Maintainer role, use a capital "M." Do not use the phrase, "if you are a maintainer" +to mean someone who is assigned the Maintainer role. Instead, write it out. "If you are assigned the Maintainer role..." + +Do not use "Maintainer permissions." A user who is assigned the Maintainer role has a set of associated permissions. + ## may, might **Might** means something has the probability of occurring. **May** gives permission to do something. Consider **can** instead of **may**. @@ -76,6 +97,13 @@ Do not use Latin abbreviations. Use **that is** instead. ([Vale](../testing.md#v Do not use first-person singular. Use **you**, **we**, or **us** instead. ([Vale](../testing.md#vale) rule: [`FirstPerson.yml`](https://gitlab.com/gitlab-org/gitlab/-/blob/master/doc/.vale/gitlab/FirstPerson.yml)) +## Owner + +When writing about the Owner role, use a capital "M." Do not use the phrase, "if you are an owner" +to mean someone who is assigned the Owner role. Instead, write it out. "If you are assigned the Owner role..." + +Do not use "Owner permissions." A user who is assigned the Owner role has a set of associated permissions. + ## permissions Do not use roles and permissions interchangeably. Each user is assigned a role. Each role includes a set of permissions. @@ -88,6 +116,13 @@ Do not use. For details, see the [Microsoft style guide](https://docs.microsoft. Do not use. Doing so may negatively affect other users and contributors, which is contrary to the GitLab value of [Diversity, Inclusion, and Belonging](https://about.gitlab.com/handbook/values/#diversity-inclusion). +## Reporter + +When writing about the Reporter role, use a capital "R." Do not use the phrase, "if you are a reporter" +to mean someone who is assigned the Reporter role. Instead, write it out. "If you are assigned the Reporter role..." + +Do not use "Reporter permissions." A user who is assigned the Reporter role has a set of associated permissions. + ## roles Do not use roles and permissions interchangeably. Each user is assigned a role. Each role includes a set of permissions. diff --git a/doc/user/application_security/container_scanning/index.md b/doc/user/application_security/container_scanning/index.md index 0fda30be9ba..48d7d68996e 100644 --- a/doc/user/application_security/container_scanning/index.md +++ b/doc/user/application_security/container_scanning/index.md @@ -46,30 +46,12 @@ To enable container scanning in your pipeline, you need the following: or [`kubernetes`](https://docs.gitlab.com/runner/install/kubernetes.html) executor. - Docker `18.09.03` or higher installed on the same computer as the runner. If you're using the shared runners on GitLab.com, then this is already the case. -- An image matching the [supported distributions](https://aquasecurity.github.io/trivy/latest/vuln-detection/os/)). +- An image matching the [supported distributions](#supported-distributions). - [Build and push](../../packages/container_registry/index.md#build-and-push-by-using-gitlab-cicd) - your Docker image to your project's container registry. The name of the Docker image should use - the following [predefined CI/CD variables](../../../ci/variables/predefined_variables.md): - - ```plaintext - $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA - ``` - - You can use these directly in your `.gitlab-ci.yml` file: - - ```yaml - build: - image: docker:19.03.12 - stage: build - services: - - docker:19.03.12-dind - variables: - IMAGE_TAG: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA - script: - - docker login -u "$CI_REGISTRY_USER" -p "$CI_REGISTRY_PASSWORD" $CI_REGISTRY - - docker build -t $IMAGE_TAG . - - docker push $IMAGE_TAG - ``` + the Docker image to your project's container registry. If using a third-party container + registry, you might need to provide authentication credentials using the `DOCKER_USER` and + `DOCKER_PASSWORD` [configuration variables](#available-cicd-variables). +- The name of the Docker image to scan, in the `DOCKER_IMAGE` [configuration variable](#available-cicd-variables). ## Configuration @@ -80,6 +62,9 @@ How you enable container scanning depends on your GitLab version: that comes with your GitLab installation. - GitLab versions earlier than 11.9: Copy and use the job from the [`Container-Scanning.gitlab-ci.yml` template](https://gitlab.com/gitlab-org/gitlab/-/blob/master/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml). + +Other changes: + - GitLab 13.6 [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/263482) better support for [FIPS](https://csrc.nist.gov/publications/detail/fips/140/2/final) by upgrading the `CS_MAJOR_VERSION` from `2` to `3`. Version `3` of the `container_scanning` Docker image uses @@ -117,21 +102,14 @@ that you can download and analyze later. When downloading, you always receive th artifact. The following is a sample `.gitlab-ci.yml` that builds your Docker image, pushes it to the container -registry, and scans the containers: +registry, and scans the image: ```yaml -variables: - DOCKER_DRIVER: overlay2 - -stages: - - build - - test - build: - image: docker:stable + image: docker:latest stage: build services: - - docker:19.03.12-dind + - docker:dind variables: IMAGE: $CI_REGISTRY_IMAGE/$CI_COMMIT_REF_SLUG:$CI_COMMIT_SHA script: @@ -183,6 +161,12 @@ You can [configure](#customizing-the-container-scanning-settings) analyzers by u | `DOCKERFILE_PATH` | `Dockerfile` | The path to the `Dockerfile` to use for generating remediations. By default, the scanner looks for a file named `Dockerfile` in the root directory of the project. You should configure this variable only if your `Dockerfile` is in a non-standard location, such as a subdirectory. See [Solutions for vulnerabilities](#solutions-for-vulnerabilities-auto-remediation) for more details. | All | | `SECURE_LOG_LEVEL` | `info` | Set the minimum logging level. Messages of this logging level or higher are output. From highest to lowest severity, the logging levels are: `fatal`, `error`, `warn`, `info`, `debug`. [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/10880) in GitLab 13.1. | All | +### Supported distributions + +Support depends on the scanner: + +- [Trivy](https://aquasecurity.github.io/trivy/latest/vuln-detection/os/) (Default). + ### Overriding the container scanning template If you want to override the job definition (for example, to change properties like `variables`), you diff --git a/lib/banzai/filter/references/label_reference_filter.rb b/lib/banzai/filter/references/label_reference_filter.rb index bf6b3e47d3b..12afece6e53 100644 --- a/lib/banzai/filter/references/label_reference_filter.rb +++ b/lib/banzai/filter/references/label_reference_filter.rb @@ -8,21 +8,57 @@ module Banzai self.reference_type = :label self.object_class = Label + def parent_records(parent, ids) + return Label.none unless parent.is_a?(Project) || parent.is_a?(Group) + + labels = find_labels(parent) + label_ids = ids.map {|y| y[:label_id]}.compact + label_names = ids.map {|y| y[:label_name]}.compact + id_relation = labels.where(id: label_ids) + label_relation = labels.where(title: label_names) + + Label.from_union([id_relation, label_relation]) + end + def find_object(parent_object, id) - find_labels(parent_object).find(id) + key = reference_cache.records_per_parent[parent_object].keys.find do |k| + k[:label_id] == id[:label_id] || k[:label_name] == id[:label_name] + end + + reference_cache.records_per_parent[parent_object][key] if key + end + + # Transform a symbol extracted from the text to a meaningful value + # + # This method has the contract that if a string `ref` refers to a + # record `record`, then `parse_symbol(ref) == record_identifier(record)`. + # + # This contract is slightly broken here, as we only have either the label_id + # or the label_name, but not both. But below, we have both pieces of information. + # But it's accounted for in `find_object` + def parse_symbol(symbol, match_data) + { label_id: match_data[:label_id]&.to_i, label_name: match_data[:label_name]&.tr('"', '') } + end + + # We assume that most classes are identifying records by ID. + # + # This method has the contract that if a string `ref` refers to a + # record `record`, then `class.parse_symbol(ref) == record_identifier(record)`. + # See note in `parse_symbol` above + def record_identifier(record) + { label_id: record.id, label_name: record.title } end def references_in(text, pattern = Label.reference_pattern) labels = {} - unescaped_html = unescape_html_entities(text).gsub(pattern) do |match| - namespace = $~[:namespace] - project = $~[:project] - project_path = reference_cache.full_project_path(namespace, project) - label = find_label_cached(project_path, $~[:label_id], $~[:label_name]) - - if label - labels[label.id] = yield match, label.id, project, namespace, $~ - "#{REFERENCE_PLACEHOLDER}#{label.id}" + + unescaped_html = unescape_html_entities(text).gsub(pattern).with_index do |match, index| + ident = identifier($~) + label = yield match, ident, $~[:project], $~[:namespace], $~ + + if label != match + labels[index] = label + "#{REFERENCE_PLACEHOLDER}#{index}" else match end @@ -33,20 +69,6 @@ module Banzai escape_with_placeholders(unescaped_html, labels) end - def find_label_cached(parent_ref, label_id, label_name) - cached_call(:banzai_find_label_cached, label_name&.tr('"', '') || label_id, path: [object_class, parent_ref]) do - find_label(parent_ref, label_id, label_name) - end - end - - def find_label(parent_ref, label_id, label_name) - parent = parent_from_ref(parent_ref) - return unless parent - - label_params = label_params(label_id, label_name) - find_labels(parent).find_by(label_params) - end - def find_labels(parent) params = if parent.is_a?(Group) { group_id: parent.id, @@ -60,21 +82,6 @@ module Banzai LabelsFinder.new(nil, params).execute(skip_authorization: true) end - # Parameters to pass to `Label.find_by` based on the given arguments - # - # id - Integer ID to pass. If present, returns {id: id} - # name - String name to pass. If `id` is absent, finds by name without - # surrounding quotes. - # - # Returns a Hash. - def label_params(id, name) - if name - { name: name.tr('"', '') } - else - { id: id.to_i } - end - end - def url_for_object(label, parent) label_url_method = if context[:label_url_method] @@ -121,6 +128,14 @@ module Banzai presenter = object.present(issuable_subject: project || group) LabelsHelper.label_tooltip_title(presenter) end + + def parent + project || group + end + + def requires_unescaping? + true + end end end end diff --git a/lib/banzai/filter/references/reference_cache.rb b/lib/banzai/filter/references/reference_cache.rb index ab0c74e00d9..24b8b4984cd 100644 --- a/lib/banzai/filter/references/reference_cache.rb +++ b/lib/banzai/filter/references/reference_cache.rb @@ -29,15 +29,15 @@ module Banzai refs = Hash.new { |hash, key| hash[key] = Set.new } nodes.each do |node| - node.to_html.scan(regex) do - path = if parent_type == :project - full_project_path($~[:namespace], $~[:project]) - else - full_group_path($~[:group]) - end + prepare_node_for_scan(node).scan(regex) do + parent_path = if parent_type == :project + full_project_path($~[:namespace], $~[:project]) + else + full_group_path($~[:group]) + end ident = filter.identifier($~) - refs[path] << ident if ident + refs[parent_path] << ident if ident end end @@ -55,9 +55,23 @@ module Banzai @per_reference ||= {} @per_reference[parent_type] ||= begin - refs = references_per_parent.keys.to_set + refs = references_per_parent.keys + parent_ref = {} - find_for_paths(refs.to_a).index_by(&:full_path) + # if we already have a parent, no need to query it again + refs.each do |ref| + next unless ref + + if context[:project]&.full_path == ref + parent_ref[ref] = context[:project] + elsif context[:group]&.full_path == ref + parent_ref[ref] = context[:group] + end + + refs -= [ref] if parent_ref[ref] + end + + find_for_paths(refs).index_by(&:full_path).merge(parent_ref) end end @@ -87,7 +101,7 @@ module Banzai @_records_per_project[filter.object_class.to_s.underscore] end - def relation_for_paths(paths) + def objects_for_paths(paths) klass = parent_type.to_s.camelize.constantize result = klass.where_full_path_in(paths) return result if parent_type == :group @@ -102,7 +116,7 @@ module Banzai to_query = paths - cache.keys unless to_query.empty? - records = relation_for_paths(to_query) + records = objects_for_paths(to_query) found = [] records.each do |record| @@ -119,7 +133,7 @@ module Banzai cache.slice(*paths).values.compact else - relation_for_paths(paths) + objects_for_paths(paths) end end @@ -170,6 +184,16 @@ module Banzai def refs_cache Gitlab::SafeRequestStore["banzai_#{parent_type}_refs".to_sym] ||= {} end + + def prepare_node_for_scan(node) + html = node.to_html + + filter.requires_unescaping? ? unescape_html_entities(html) : html + end + + def unescape_html_entities(text) + CGI.unescapeHTML(text.to_s) + end end end end diff --git a/lib/banzai/filter/references/reference_filter.rb b/lib/banzai/filter/references/reference_filter.rb index 58436f4505e..6c2c993cc01 100644 --- a/lib/banzai/filter/references/reference_filter.rb +++ b/lib/banzai/filter/references/reference_filter.rb @@ -109,6 +109,10 @@ module Banzai context[:group] end + def requires_unescaping? + false + end + private # Returns a data attribute String to attach to a reference link diff --git a/scripts/verify-tff-mapping b/scripts/verify-tff-mapping index e18a14e17d6..4555a9854dd 100755 --- a/scripts/verify-tff-mapping +++ b/scripts/verify-tff-mapping @@ -84,8 +84,8 @@ tests = [ { explanation: 'FOSS views should map to respective spec', - source: 'app/views/admin/users/_user.html.haml', - expected: ['spec/views/admin/users/_user.html.haml_spec.rb'] + source: 'app/views/admin/dashboard/index.html.haml', + expected: ['spec/views/admin/dashboard/index.html.haml_spec.rb'] }, { diff --git a/spec/features/admin/users/user_spec.rb b/spec/features/admin/users/user_spec.rb index 01341398135..3599658ee56 100644 --- a/spec/features/admin/users/user_spec.rb +++ b/spec/features/admin/users/user_spec.rb @@ -356,27 +356,19 @@ RSpec.describe 'Admin::Users::User' do end end - [true, false].each do |vue_admin_users| - context "with vue_admin_users feature flag set to #{vue_admin_users}", js: vue_admin_users do - before do - stub_feature_flags(vue_admin_users: vue_admin_users) - end - - describe 'GET /admin/users' do - context 'user pending approval' do - it 'shows user info', :aggregate_failures do - user = create(:user, :blocked_pending_approval) + describe 'GET /admin/users', :js do + context 'user pending approval' do + it 'shows user info', :aggregate_failures do + user = create(:user, :blocked_pending_approval) - visit admin_users_path - click_link 'Pending approval' - click_link user.name + visit admin_users_path + click_link 'Pending approval' + click_link user.name - expect(page).to have_content(user.name) - expect(page).to have_content('Pending approval') - expect(page).to have_link('Approve user') - expect(page).to have_link('Reject request') - end - end + expect(page).to have_content(user.name) + expect(page).to have_content('Pending approval') + expect(page).to have_link('Approve user') + expect(page).to have_link('Reject request') end end end diff --git a/spec/features/admin/users/users_spec.rb b/spec/features/admin/users/users_spec.rb index d3931373ee3..790c9a4fc74 100644 --- a/spec/features/admin/users/users_spec.rb +++ b/spec/features/admin/users/users_spec.rb @@ -11,291 +11,300 @@ RSpec.describe 'Admin::Users' do gitlab_enable_admin_mode_sign_in(current_user) end - [true, false].each do |vue_admin_users| - context "with vue_admin_users feature flag set to #{vue_admin_users}", js: vue_admin_users do - before do - stub_feature_flags(vue_admin_users: vue_admin_users) - end + describe 'GET /admin/users', :js do + before do + visit admin_users_path + end - describe 'GET /admin/users' do - before do - visit admin_users_path - end + it "is ok" do + expect(current_path).to eq(admin_users_path) + end - it "is ok" do - expect(current_path).to eq(admin_users_path) - end + it "has users list" do + current_user.reload - it "has users list" do - current_user.reload + expect(page).to have_content(current_user.email) + expect(page).to have_content(current_user.name) + expect(page).to have_content(current_user.created_at.strftime('%e %b, %Y')) + expect(page).to have_content(user.email) + expect(page).to have_content(user.name) + expect(page).to have_content('Projects') - expect(page).to have_content(current_user.email) - expect(page).to have_content(current_user.name) - expect(page).to have_content(current_user.created_at.strftime('%e %b, %Y')) - expect(page).to have_content(user.email) - expect(page).to have_content(user.name) - expect(page).to have_content('Projects') + click_user_dropdown_toggle(user.id) - click_user_dropdown_toggle(user.id) + expect(page).to have_button('Block') + expect(page).to have_button('Deactivate') + expect(page).to have_button('Delete user') + expect(page).to have_button('Delete user and contributions') + end - expect(page).to have_button('Block') - expect(page).to have_button('Deactivate') - expect(page).to have_button('Delete user') - expect(page).to have_button('Delete user and contributions') - end + it 'clicking edit user takes us to edit page', :aggregate_failures do + page.within("[data-testid='user-actions-#{user.id}']") do + click_link 'Edit' + end - it 'clicking edit user takes us to edit page', :aggregate_failures do - page.within("[data-testid='user-actions-#{user.id}']") do - click_link 'Edit' - end + expect(page).to have_content('Name') + expect(page).to have_content('Password') + end - expect(page).to have_content('Name') - expect(page).to have_content('Password') - end + describe 'view extra user information' do + it 'shows the user popover on hover', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/11290' do + expect(page).not_to have_selector('#__BV_popover_1__') - describe 'view extra user information' do - it 'shows the user popover on hover', :js, quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/11290' do - expect(page).not_to have_selector('#__BV_popover_1__') + first_user_link = page.first('.js-user-link') + first_user_link.hover - first_user_link = page.first('.js-user-link') - first_user_link.hover + expect(page).to have_selector('#__BV_popover_1__') + end + end - expect(page).to have_selector('#__BV_popover_1__') - end - end + context 'user project count' do + before do + project = create(:project) + project.add_maintainer(current_user) + end + + it 'displays count of users projects' do + visit admin_users_path - context 'user project count' do - before do - project = create(:project) - project.add_maintainer(current_user) - end + expect(page.find("[data-testid='user-project-count-#{current_user.id}']").text).to eq("1") + end + end - it 'displays count of users projects' do - visit admin_users_path + describe 'tabs' do + it 'has multiple tabs to filter users' do + expect(page).to have_link('Active', href: admin_users_path) + expect(page).to have_link('Admins', href: admin_users_path(filter: 'admins')) + expect(page).to have_link('2FA Enabled', href: admin_users_path(filter: 'two_factor_enabled')) + expect(page).to have_link('2FA Disabled', href: admin_users_path(filter: 'two_factor_disabled')) + expect(page).to have_link('External', href: admin_users_path(filter: 'external')) + expect(page).to have_link('Blocked', href: admin_users_path(filter: 'blocked')) + expect(page).to have_link('Deactivated', href: admin_users_path(filter: 'deactivated')) + expect(page).to have_link('Without projects', href: admin_users_path(filter: 'wop')) + end - expect(page.find("[data-testid='user-project-count-#{current_user.id}']").text).to eq("1") - end + context '`Pending approval` tab' do + before do + visit admin_users_path end - describe 'tabs' do - it 'has multiple tabs to filter users' do - expect(page).to have_link('Active', href: admin_users_path) - expect(page).to have_link('Admins', href: admin_users_path(filter: 'admins')) - expect(page).to have_link('2FA Enabled', href: admin_users_path(filter: 'two_factor_enabled')) - expect(page).to have_link('2FA Disabled', href: admin_users_path(filter: 'two_factor_disabled')) - expect(page).to have_link('External', href: admin_users_path(filter: 'external')) - expect(page).to have_link('Blocked', href: admin_users_path(filter: 'blocked')) - expect(page).to have_link('Banned', href: admin_users_path(filter: 'banned')) - expect(page).to have_link('Deactivated', href: admin_users_path(filter: 'deactivated')) - expect(page).to have_link('Without projects', href: admin_users_path(filter: 'wop')) - end - - context '`Pending approval` tab' do - before do - visit admin_users_path - end - - it 'shows the `Pending approval` tab' do - expect(page).to have_link('Pending approval', href: admin_users_path(filter: 'blocked_pending_approval')) - end - end + it 'shows the `Pending approval` tab' do + expect(page).to have_link('Pending approval', href: admin_users_path(filter: 'blocked_pending_approval')) end + end + end + + describe 'search and sort' do + before_all do + create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago) + create(:user, name: 'Foo Baz', last_activity_on: 2.days.ago) + create(:user, name: 'Dmitriy') + end - describe 'search and sort' do - before_all do - create(:user, name: 'Foo Bar', last_activity_on: 3.days.ago) - create(:user, name: 'Foo Baz', last_activity_on: 2.days.ago) - create(:user, name: 'Dmitriy') - end + it 'searches users by name' do + visit admin_users_path(search_query: 'Foo') - it 'searches users by name' do - visit admin_users_path(search_query: 'Foo') + expect(page).to have_content('Foo Bar') + expect(page).to have_content('Foo Baz') + expect(page).not_to have_content('Dmitriy') + end - expect(page).to have_content('Foo Bar') - expect(page).to have_content('Foo Baz') - expect(page).not_to have_content('Dmitriy') - end + it 'sorts users by name' do + visit admin_users_path - it 'sorts users by name' do - visit admin_users_path + sort_by('Name') - sort_by('Name') + expect(first_row.text).to include('Dmitriy') + expect(second_row.text).to include('Foo Bar') + end - expect(first_row.text).to include('Dmitriy') - expect(second_row.text).to include('Foo Bar') - end + it 'sorts search results only' do + visit admin_users_path(search_query: 'Foo') - it 'sorts search results only' do - visit admin_users_path(search_query: 'Foo') + sort_by('Name') + expect(page).not_to have_content('Dmitriy') + expect(first_row.text).to include('Foo Bar') + expect(second_row.text).to include('Foo Baz') + end - sort_by('Name') - expect(page).not_to have_content('Dmitriy') - expect(first_row.text).to include('Foo Bar') - expect(second_row.text).to include('Foo Baz') - end + it 'searches with respect of sorting' do + visit admin_users_path(sort: 'Name') + + fill_in :search_query, with: 'Foo' + click_button('Search users') + + expect(first_row.text).to include('Foo Bar') + expect(second_row.text).to include('Foo Baz') + end - it 'searches with respect of sorting' do - visit admin_users_path(sort: 'Name') + it 'sorts users by recent last activity' do + visit admin_users_path(search_query: 'Foo') - fill_in :search_query, with: 'Foo' - click_button('Search users') + sort_by('Recent last activity') - expect(first_row.text).to include('Foo Bar') - expect(second_row.text).to include('Foo Baz') - end + expect(first_row.text).to include('Foo Baz') + expect(second_row.text).to include('Foo Bar') + end - it 'sorts users by recent last activity' do - visit admin_users_path(search_query: 'Foo') + it 'sorts users by oldest last activity' do + visit admin_users_path(search_query: 'Foo') - sort_by('Recent last activity') + sort_by('Oldest last activity') - expect(first_row.text).to include('Foo Baz') - expect(second_row.text).to include('Foo Bar') - end + expect(first_row.text).to include('Foo Bar') + expect(second_row.text).to include('Foo Baz') + end + end - it 'sorts users by oldest last activity' do - visit admin_users_path(search_query: 'Foo') + describe 'Two-factor Authentication filters' do + it 'counts users who have enabled 2FA' do + create(:user, :two_factor) - sort_by('Oldest last activity') + visit admin_users_path - expect(first_row.text).to include('Foo Bar') - expect(second_row.text).to include('Foo Baz') - end + page.within('.filter-two-factor-enabled small') do + expect(page).to have_content('1') end + end - describe 'Two-factor Authentication filters' do - it 'counts users who have enabled 2FA' do - create(:user, :two_factor) + it 'filters by users who have enabled 2FA' do + user = create(:user, :two_factor) - visit admin_users_path + visit admin_users_path + click_link '2FA Enabled' - page.within('.filter-two-factor-enabled small') do - expect(page).to have_content('1') - end - end + expect(page).to have_content(user.email) + end - it 'filters by users who have enabled 2FA' do - user = create(:user, :two_factor) + it 'counts users who have not enabled 2FA' do + visit admin_users_path - visit admin_users_path - click_link '2FA Enabled' + page.within('.filter-two-factor-disabled small') do + expect(page).to have_content('2') # Including admin + end + end - expect(page).to have_content(user.email) - end + it 'filters by users who have not enabled 2FA' do + visit admin_users_path + click_link '2FA Disabled' - it 'counts users who have not enabled 2FA' do - visit admin_users_path + expect(page).to have_content(user.email) + end + end - page.within('.filter-two-factor-disabled small') do - expect(page).to have_content('2') # Including admin - end - end + describe 'Pending approval filter' do + it 'counts users who are pending approval' do + create_list(:user, 2, :blocked_pending_approval) - it 'filters by users who have not enabled 2FA' do - visit admin_users_path - click_link '2FA Disabled' + visit admin_users_path - expect(page).to have_content(user.email) - end + page.within('.filter-blocked-pending-approval small') do + expect(page).to have_content('2') end + end - describe 'Pending approval filter' do - it 'counts users who are pending approval' do - create_list(:user, 2, :blocked_pending_approval) + it 'filters by users who are pending approval' do + user = create(:user, :blocked_pending_approval) - visit admin_users_path + visit admin_users_path + click_link 'Pending approval' - page.within('.filter-blocked-pending-approval small') do - expect(page).to have_content('2') - end - end + expect(page).to have_content(user.email) + end + end - it 'filters by users who are pending approval' do - user = create(:user, :blocked_pending_approval) + context 'when blocking/unblocking a user' do + it 'shows confirmation and allows blocking and unblocking', :js do + expect(page).to have_content(user.email) - visit admin_users_path - click_link 'Pending approval' + click_action_in_user_dropdown(user.id, 'Block') - expect(page).to have_content(user.email) - end - end + wait_for_requests + + expect(page).to have_content('Block user') + expect(page).to have_content('Blocking user has the following effects') + expect(page).to have_content('User will not be able to login') + expect(page).to have_content('Owned groups will be left') - context 'when blocking/unblocking a user' do - it 'shows confirmation and allows blocking and unblocking', :js do - expect(page).to have_content(user.email) + find('.modal-footer button', text: 'Block').click - click_action_in_user_dropdown(user.id, 'Block') + wait_for_requests - wait_for_requests + expect(page).to have_content('Successfully blocked') + expect(page).not_to have_content(user.email) - expect(page).to have_content('Block user') - expect(page).to have_content('Blocking user has the following effects') - expect(page).to have_content('User will not be able to login') - expect(page).to have_content('Owned groups will be left') + click_link 'Blocked' - find('.modal-footer button', text: 'Block').click + wait_for_requests - wait_for_requests + expect(page).to have_content(user.email) - expect(page).to have_content('Successfully blocked') - expect(page).not_to have_content(user.email) + click_action_in_user_dropdown(user.id, 'Unblock') - click_link 'Blocked' + expect(page).to have_content('Unblock user') + expect(page).to have_content('You can always block their account again if needed.') - wait_for_requests + find('.modal-footer button', text: 'Unblock').click - expect(page).to have_content(user.email) + wait_for_requests - click_action_in_user_dropdown(user.id, 'Unblock') + expect(page).to have_content('Successfully unblocked') + expect(page).not_to have_content(user.email) + end + end - expect(page).to have_content('Unblock user') - expect(page).to have_content('You can always block their account again if needed.') + context 'when deactivating/re-activating a user' do + it 'shows confirmation and allows deactivating and re-activating', :js do + expect(page).to have_content(user.email) - find('.modal-footer button', text: 'Unblock').click + click_action_in_user_dropdown(user.id, 'Deactivate') - wait_for_requests + expect(page).to have_content('Deactivate user') + expect(page).to have_content('Deactivating a user has the following effects') + expect(page).to have_content('The user will be logged out') + expect(page).to have_content('Personal projects, group and user history will be left intact') - expect(page).to have_content('Successfully unblocked') - expect(page).not_to have_content(user.email) - end - end + find('.modal-footer button', text: 'Deactivate').click - context 'when deactivating/re-activating a user' do - it 'shows confirmation and allows deactivating and re-activating', :js do - expect(page).to have_content(user.email) + wait_for_requests - click_action_in_user_dropdown(user.id, 'Deactivate') + expect(page).to have_content('Successfully deactivated') + expect(page).not_to have_content(user.email) - expect(page).to have_content('Deactivate user') - expect(page).to have_content('Deactivating a user has the following effects') - expect(page).to have_content('The user will be logged out') - expect(page).to have_content('Personal projects, group and user history will be left intact') + click_link 'Deactivated' - find('.modal-footer button', text: 'Deactivate').click + wait_for_requests - wait_for_requests + expect(page).to have_content(user.email) - expect(page).to have_content('Successfully deactivated') - expect(page).not_to have_content(user.email) + click_action_in_user_dropdown(user.id, 'Activate') - click_link 'Deactivated' + expect(page).to have_content('Activate user') + expect(page).to have_content('You can always deactivate their account again if needed.') - wait_for_requests + find('.modal-footer button', text: 'Activate').click - expect(page).to have_content(user.email) + wait_for_requests - click_action_in_user_dropdown(user.id, 'Activate') + expect(page).to have_content('Successfully activated') + expect(page).not_to have_content(user.email) + end + end - expect(page).to have_content('Activate user') - expect(page).to have_content('You can always deactivate their account again if needed.') + describe 'internal users' do + context 'when showing a `Ghost User`' do + let_it_be(:ghost_user) { create(:user, :ghost) } - find('.modal-footer button', text: 'Activate').click + it 'does not render actions dropdown' do + expect(page).not_to have_css("[data-testid='user-actions-#{ghost_user.id}'] [data-testid='dropdown-toggle']") + end + end - wait_for_requests + context 'when showing a `Bot User`' do + let_it_be(:bot_user) { create(:user, user_type: :alert_bot) } - expect(page).to have_content('Successfully activated') - expect(page).not_to have_content(user.email) - end + it 'does not render actions dropdown' do + expect(page).not_to have_css("[data-testid='user-actions-#{bot_user.id}'] [data-testid='dropdown-toggle']") end end end diff --git a/spec/finders/deployments_finder_spec.rb b/spec/finders/deployments_finder_spec.rb index b294f1117f5..bd03b254f40 100644 --- a/spec/finders/deployments_finder_spec.rb +++ b/spec/finders/deployments_finder_spec.rb @@ -81,16 +81,6 @@ RSpec.describe DeploymentsFinder do it 'returns deployments with matched updated_at' do is_expected.to match_array([deployment_2, deployment_1]) end - - context 'when deployments_finder_implicitly_enforce_ordering_for_updated_at_filter feature flag is disabled' do - before do - stub_feature_flags(deployments_finder_implicitly_enforce_ordering_for_updated_at_filter: false) - end - - it 'returns deployments with matched updated_at' do - is_expected.to match_array([deployment_1, deployment_2]) - end - end end context 'when the environment name is specified' do @@ -244,20 +234,6 @@ RSpec.describe DeploymentsFinder do expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:updated_at].asc.to_sql) expect(subject.order_values.second.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql) end - - context 'when deployments_finder_implicitly_enforce_ordering_for_updated_at_filter feature flag is disabled' do - before do - stub_feature_flags(deployments_finder_implicitly_enforce_ordering_for_updated_at_filter: false) - end - - it 'sorts by only one column' do - expect(subject.order_values.size).to eq(1) - end - - it 'sorts by `id`' do - expect(subject.order_values.first.to_sql).to eq(Deployment.arel_table[:id].asc.to_sql) - end - end end context 'when filtering by finished time' do diff --git a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb index db7dda96cad..b18d68c8dd4 100644 --- a/spec/lib/banzai/filter/references/label_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/label_reference_filter_spec.rb @@ -702,4 +702,72 @@ RSpec.describe Banzai::Filter::References::LabelReferenceFilter do expect(result.css('a').first.text).to eq "#{label.name} in #{project.full_name}" end end + + context 'checking N+1' do + let_it_be(:group) { create(:group) } + let_it_be(:group2) { create(:group) } + let_it_be(:project) { create(:project, :public, namespace: group) } + let_it_be(:project2) { create(:project, :public, namespace: group2) } + let_it_be(:project3) { create(:project, :public) } + let_it_be(:project_label) { create(:label, project: project) } + let_it_be(:project_label2) { create(:label, project: project) } + let_it_be(:project2_label) { create(:label, project: project2) } + let_it_be(:group2_label) { create(:group_label, group: group2, color: '#00ff00') } + let_it_be(:project_reference) { "#{project_label.to_reference}" } + let_it_be(:project_reference2) { "#{project_label2.to_reference}" } + let_it_be(:project2_reference) { "#{project2_label.to_reference}" } + let_it_be(:group2_reference) { "#{project2.full_path}~#{group2_label.name}" } + + it 'does not have N+1 per multiple references per project', :use_sql_query_cache do + markdown = "#{project_reference}" + control_count = 1 + + expect do + reference_filter(markdown) + end.not_to exceed_all_query_limit(control_count) + + markdown = "#{project_reference} ~qwert ~werty ~ertyu ~rtyui #{project_reference2}" + + expect do + reference_filter(markdown) + end.not_to exceed_all_query_limit(control_count) + end + + it 'has N+1 for multiple unique project/group references', :use_sql_query_cache do + # reference to already loaded project, only one query + markdown = "#{project_reference}" + control_count = 1 + + expect do + reference_filter(markdown, project: project) + end.not_to exceed_all_query_limit(control_count) + + # Since we're not batching label queries across projects/groups, + # queries increase when a new project/group is added. + # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359 + # first reference to already loaded project (1), + # second reference requires project and namespace (2), and label (1) + markdown = "#{project_reference} #{group2_reference}" + max_count = control_count + 3 + + expect do + reference_filter(markdown) + end.not_to exceed_all_query_limit(max_count) + + # third reference to already queried project/namespace, nothing extra (no N+1 here) + markdown = "#{project_reference} #{group2_reference} #{project2_reference}" + + expect do + reference_filter(markdown) + end.not_to exceed_all_query_limit(max_count) + + # last reference needs another namespace and label query (2) + markdown = "#{project_reference} #{group2_reference} #{project2_reference} #{project3.full_path}~test_label" + max_count += 2 + + expect do + reference_filter(markdown) + end.not_to exceed_all_query_limit(max_count) + end + end end diff --git a/spec/lib/banzai/filter/references/reference_cache_spec.rb b/spec/lib/banzai/filter/references/reference_cache_spec.rb index 9e2a6f35910..c9404c381d3 100644 --- a/spec/lib/banzai/filter/references/reference_cache_spec.rb +++ b/spec/lib/banzai/filter/references/reference_cache_spec.rb @@ -55,11 +55,12 @@ RSpec.describe Banzai::Filter::References::ReferenceCache do cache_single.load_records_per_parent end.count + expect(control_count).to eq 1 + # Since this is an issue filter that is not batching issue queries # across projects, we have to account for that. - # 1 for both projects, 1 for issues in each project == 3 - # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359 - max_count = control_count + 1 + # 1 for original issue, 2 for second route/project, 1 for other issue + max_count = control_count + 3 expect do cache.load_references_per_parent(filter.nodes) diff --git a/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb index 7ab3b24b1c2..2e324669870 100644 --- a/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb +++ b/spec/lib/banzai/filter/references/snippet_reference_filter_spec.rb @@ -233,13 +233,15 @@ RSpec.describe Banzai::Filter::References::SnippetReferenceFilter do reference_filter(markdown) end.count + expect(control_count).to eq 1 + markdown = "#{reference} $9999990 $9999991 $9999992 $9999993 #{reference2} something/cool$12" # Since we're not batching snippet queries across projects, # we have to account for that. # 1 for both projects, 1 for snippets in each project == 3 # TODO: https://gitlab.com/gitlab-org/gitlab/-/issues/330359 - max_count = control_count + 1 + max_count = control_count + 2 expect do reference_filter(markdown) diff --git a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb index bc5e6ea7bb3..9f6851ecfbd 100644 --- a/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb +++ b/spec/lib/gitlab/import_export/project/tree_restorer_spec.rb @@ -600,9 +600,8 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do setup_import_export_config('light') setup_reader(reader) - expect(project) - .to receive(:merge_requests) - .and_raise(exception) + expect(project).to receive(:merge_requests).and_call_original + expect(project).to receive(:merge_requests).and_raise(exception) end it 'report post import error' do @@ -618,12 +617,9 @@ RSpec.describe Gitlab::ImportExport::Project::TreeRestorer do setup_import_export_config('light') setup_reader(reader) - expect(project) - .to receive(:merge_requests) - .and_raise(exception) - expect(project) - .to receive(:merge_requests) - .and_call_original + expect(project).to receive(:merge_requests).and_call_original + expect(project).to receive(:merge_requests).and_raise(exception) + expect(project).to receive(:merge_requests).and_call_original expect(restored_project_json).to eq(true) end diff --git a/spec/views/admin/users/_user.html.haml_spec.rb b/spec/views/admin/users/_user.html.haml_spec.rb deleted file mode 100644 index aed05e4ea9b..00000000000 --- a/spec/views/admin/users/_user.html.haml_spec.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'admin/users/_user.html.haml' do - before do - allow(view).to receive(:user).and_return(user) - end - - context 'internal users' do - context 'when showing a `Ghost User`' do - let(:user) { create(:user, :ghost) } - - it 'does not render action buttons' do - render - - expect(rendered).not_to have_selector('.table-action-buttons') - end - end - - context 'when showing a `Bot User`' do - let(:user) { create(:user, user_type: :alert_bot) } - - it 'does not render action buttons' do - render - - expect(rendered).not_to have_selector('.table-action-buttons') - end - end - - context 'when showing a `Migration User`' do - let(:user) { create(:user, user_type: :migration_bot) } - - it 'does not render action buttons' do - render - - expect(rendered).not_to have_selector('.table-action-buttons') - end - end - end - - context 'when showing an external user' do - let(:user) { create(:user) } - - it 'renders action buttons' do - render - - expect(rendered).to have_selector('.table-action-buttons') - end - end -end diff --git a/spec/workers/ssh_keys/expired_notification_worker_spec.rb b/spec/workers/ssh_keys/expired_notification_worker_spec.rb index 0fc999a81d6..84f8f8d7b83 100644 --- a/spec/workers/ssh_keys/expired_notification_worker_spec.rb +++ b/spec/workers/ssh_keys/expired_notification_worker_spec.rb @@ -15,6 +15,20 @@ RSpec.describe SshKeys::ExpiredNotificationWorker, type: :worker do describe '#perform' do let_it_be(:user) { create(:user) } + context 'with a large batch' do + before do + stub_const("SshKeys::ExpiredNotificationWorker::BATCH_SIZE", 5) + end + + let_it_be_with_reload(:keys) { create_list(:key, 20, expires_at: 3.days.ago, user: user) } + + it 'updates all keys regardless of batch size' do + worker.perform + + expect(keys.pluck(:expiry_notification_delivered_at)).not_to include(nil) + end + end + context 'with expiring key today' do let_it_be_with_reload(:expired_today) { create(:key, expires_at: Time.current, user: user) } diff --git a/vendor/project_templates/cluster_management.tar.gz b/vendor/project_templates/cluster_management.tar.gz Binary files differindex f4afa928d1c..d24470f1642 100644 --- a/vendor/project_templates/cluster_management.tar.gz +++ b/vendor/project_templates/cluster_management.tar.gz |