diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-04 12:09:18 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-10-04 12:09:18 +0300 |
commit | 0d8bcdf77d609b3624541de767a0129aa0b7e8d2 (patch) | |
tree | 40a5aebae63c322c38660537adc433fc80dbb46d | |
parent | c99b40d5a7f93e2d51c3716676ff7c345ca19f06 (diff) |
Add latest changes from gitlab-org/gitlab@master
57 files changed, 837 insertions, 405 deletions
diff --git a/.rubocop.yml b/.rubocop.yml index 21e2f8f2827..5d5d6094bad 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -785,3 +785,21 @@ Gemspec/AvoidExecutingGit: Lint/BinaryOperatorWithIdenticalOperands: Exclude: - '{,ee/,qa/}spec/**/*_{spec,shared_examples,shared_context}.rb' + +Cop/SidekiqRedisCall: + Enabled: true + Exclude: + - '{,ee/,jh/}spec/**/*' + - 'lib/gitlab/database/migration_helpers.rb' + - 'lib/gitlab/sidekiq_migrate_jobs.rb' + - 'lib/gitlab/sidekiq_versioning.rb' + +Cop/RedisQueueUsage: + Enabled: true + Exclude: + - '{,ee/,jh/}spec/**/*' + - 'config/initializers/sidekiq.rb' + - 'lib/gitlab/instrumentation/redis.rb' + - 'lib/gitlab/redis.rb' + - 'lib/system_check/app/redis_version_check.rb' + - 'lib/gitlab/mail_room.rb' diff --git a/app/assets/javascripts/milestones/milestone_select.js b/app/assets/javascripts/milestones/milestone_select.js index c95ec3dd10b..d4876c3dbe8 100644 --- a/app/assets/javascripts/milestones/milestone_select.js +++ b/app/assets/javascripts/milestones/milestone_select.js @@ -121,7 +121,7 @@ export default class MilestoneSelect { title: __('Started'), }); } - if (extraOptions.length) { + if (extraOptions.length && data.length) { extraOptions.push({ type: 'divider' }); } diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js deleted file mode 100644 index 86b80a0ba5b..00000000000 --- a/app/assets/javascripts/pages/admin/groups/show/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import UsersSelect from '~/users_select'; - -new UsersSelect(); // eslint-disable-line no-new diff --git a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue index f5620876783..74656a4fcd8 100644 --- a/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue +++ b/app/assets/javascripts/runner/admin_runners/admin_runners_app.vue @@ -30,7 +30,12 @@ import RunnerActionsCell from '../components/cells/runner_actions_cell.vue'; import { pausedTokenConfig } from '../components/search_tokens/paused_token_config'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; import { tagTokenConfig } from '../components/search_tokens/tag_token_config'; -import { ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, I18N_FETCH_ERROR } from '../constants'; +import { + ADMIN_FILTERED_SEARCH_NAMESPACE, + INSTANCE_TYPE, + I18N_FETCH_ERROR, + FILTER_CSS_CLASSES, +} from '../constants'; import { captureException } from '../sentry_utils'; export default { @@ -167,6 +172,7 @@ export default { }, filteredSearchNamespace: ADMIN_FILTERED_SEARCH_NAMESPACE, INSTANCE_TYPE, + FILTER_CSS_CLASSES, }; </script> <template> @@ -195,6 +201,7 @@ export default { <runner-filtered-search-bar v-model="search" + :class="$options.FILTER_CSS_CLASSES" :tokens="searchTokens" :namespace="$options.filteredSearchNamespace" /> diff --git a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue index 5a9ab21a457..da59de9a9eb 100644 --- a/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue +++ b/app/assets/javascripts/runner/components/runner_filtered_search_bar.vue @@ -85,7 +85,6 @@ export default { </script> <template> <filtered-search - class="gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1" v-bind="$attrs" :namespace="namespace" recent-searches-storage-key="runners-search" diff --git a/app/assets/javascripts/runner/components/runner_membership_toggle.vue b/app/assets/javascripts/runner/components/runner_membership_toggle.vue new file mode 100644 index 00000000000..2b37b1cc797 --- /dev/null +++ b/app/assets/javascripts/runner/components/runner_membership_toggle.vue @@ -0,0 +1,42 @@ +<script> +import { GlToggle } from '@gitlab/ui'; +import { + I18N_SHOW_ONLY_INHERITED, + MEMBERSHIP_DESCENDANTS, + MEMBERSHIP_ALL_AVAILABLE, +} from '../constants'; + +export default { + components: { + GlToggle, + }, + props: { + value: { + type: String, + default: MEMBERSHIP_DESCENDANTS, + required: false, + }, + }, + computed: { + toggle() { + return this.value === MEMBERSHIP_DESCENDANTS; + }, + }, + methods: { + onChange(value) { + this.$emit('input', value ? MEMBERSHIP_DESCENDANTS : MEMBERSHIP_ALL_AVAILABLE); + }, + }, + I18N_SHOW_ONLY_INHERITED, +}; +</script> + +<template> + <gl-toggle + data-testid="runner-membership-toggle" + :value="toggle" + :label="$options.I18N_SHOW_ONLY_INHERITED" + label-position="left" + @change="onChange" + /> +</template> diff --git a/app/assets/javascripts/runner/constants.js b/app/assets/javascripts/runner/constants.js index 624c8fa701a..5f56151ad35 100644 --- a/app/assets/javascripts/runner/constants.js +++ b/app/assets/javascripts/runner/constants.js @@ -11,6 +11,9 @@ export const RUNNER_DETAILS_JOBS_PAGE_SIZE = 30; export const I18N_FETCH_ERROR = s__('Runners|Something went wrong while fetching runner data.'); export const I18N_DETAILS_TITLE = s__('Runners|Runner #%{runner_id}'); +export const FILTER_CSS_CLASSES = + 'gl-bg-gray-10 gl-p-5 gl-border-solid gl-border-gray-100 gl-border-0 gl-border-t-1 gl-border-b-1'; + // Type export const I18N_ALL_TYPES = s__('Runners|All'); @@ -85,6 +88,7 @@ export const I18N_LOCKED_RUNNER_DESCRIPTION = s__( export const I18N_VERSION_LABEL = s__('Runners|Version %{version}'); export const I18N_LAST_CONTACT_LABEL = s__('Runners|Last contact: %{timeAgo}'); export const I18N_CREATED_AT_LABEL = s__('Runners|Created %{timeAgo}'); +export const I18N_SHOW_ONLY_INHERITED = s__('Runners|Show only inherited'); // Runner details @@ -110,6 +114,7 @@ export const PARAM_KEY_PAUSED = 'paused'; export const PARAM_KEY_RUNNER_TYPE = 'runner_type'; export const PARAM_KEY_TAG = 'tag'; export const PARAM_KEY_SEARCH = 'search'; +export const PARAM_KEY_MEMBERSHIP = 'membership'; export const PARAM_KEY_SORT = 'sort'; export const PARAM_KEY_AFTER = 'after'; @@ -142,6 +147,13 @@ export const CONTACTED_ASC = 'CONTACTED_ASC'; export const DEFAULT_SORT = CREATED_DESC; +// CiRunnerMembershipFilter + +export const MEMBERSHIP_DESCENDANTS = 'DESCENDANTS'; +export const MEMBERSHIP_ALL_AVAILABLE = 'ALL_AVAILABLE'; + +export const DEFAULT_MEMBERSHIP = MEMBERSHIP_DESCENDANTS; + // Local storage namespaces export const ADMIN_FILTERED_SEARCH_NAMESPACE = 'admin_runners'; diff --git a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql index 4c519b9b867..9294953426f 100644 --- a/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/group_runners.query.graphql @@ -2,6 +2,7 @@ query getGroupRunners( $groupFullPath: ID! + $membership: CiRunnerMembershipFilter $before: String $after: String $first: Int @@ -15,7 +16,7 @@ query getGroupRunners( group(fullPath: $groupFullPath) { id # Apollo required runners( - membership: DESCENDANTS + membership: $membership before: $before after: $after first: $first diff --git a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql index 958b4ea0dd3..e88a2c2e7e6 100644 --- a/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql +++ b/app/assets/javascripts/runner/graphql/list/group_runners_count.query.graphql @@ -1,5 +1,6 @@ query getGroupRunnersCount( $groupFullPath: ID! + $membership: CiRunnerMembershipFilter $paused: Boolean $status: CiRunnerStatus $type: CiRunnerType @@ -9,7 +10,7 @@ query getGroupRunnersCount( group(fullPath: $groupFullPath) { id # Apollo required runners( - membership: DESCENDANTS + membership: $membership paused: $paused status: $status type: $type diff --git a/app/assets/javascripts/runner/group_runners/group_runners_app.vue b/app/assets/javascripts/runner/group_runners/group_runners_app.vue index 70826a6bfa1..17cda36aca6 100644 --- a/app/assets/javascripts/runner/group_runners/group_runners_app.vue +++ b/app/assets/javascripts/runner/group_runners/group_runners_app.vue @@ -10,6 +10,7 @@ import { fromSearchToVariables, isSearchFiltered, } from 'ee_else_ce/runner/runner_search_utils'; +import glFeatureFlagMixin from '~/vue_shared/mixins/gl_feature_flags_mixin'; import groupRunnersQuery from 'ee_else_ce/runner/graphql/list/group_runners.query.graphql'; import RegistrationDropdown from '../components/registration/registration_dropdown.vue'; @@ -22,6 +23,7 @@ import RunnerStats from '../components/stat/runner_stats.vue'; import RunnerPagination from '../components/runner_pagination.vue'; import RunnerTypeTabs from '../components/runner_type_tabs.vue'; import RunnerActionsCell from '../components/cells/runner_actions_cell.vue'; +import RunnerMembershipToggle from '../components/runner_membership_toggle.vue'; import { pausedTokenConfig } from '../components/search_tokens/paused_token_config'; import { statusTokenConfig } from '../components/search_tokens/status_token_config'; @@ -30,6 +32,7 @@ import { GROUP_TYPE, PROJECT_TYPE, I18N_FETCH_ERROR, + FILTER_CSS_CLASSES, } from '../constants'; import { captureException } from '../sentry_utils'; @@ -43,11 +46,13 @@ export default { RunnerList, RunnerListEmptyState, RunnerName, + RunnerMembershipToggle, RunnerStats, RunnerPagination, RunnerTypeTabs, RunnerActionsCell, }, + mixins: [glFeatureFlagMixin()], inject: ['emptyStateSvgPath', 'emptyStateFilteredSvgPath'], props: { registrationToken: { @@ -135,6 +140,11 @@ export default { isSearchFiltered() { return isSearchFiltered(this.search); }, + shouldRenderAllAvailableToggle() { + // Feature flag for `runners_finder_all_available` + // See: https://gitlab.com/gitlab-org/gitlab/-/issues/374525 + return this.glFeatures?.runnersFinderAllAvailable; + }, }, watch: { search: { @@ -176,6 +186,7 @@ export default { }, TABS_RUNNER_TYPES: [GROUP_TYPE, PROJECT_TYPE], GROUP_TYPE, + FILTER_CSS_CLASSES, }; </script> @@ -204,11 +215,22 @@ export default { /> </div> - <runner-filtered-search-bar - v-model="search" - :tokens="searchTokens" - :namespace="filteredSearchNamespace" - /> + <div + class="gl-display-flex gl-flex-direction-column gl-md-flex-direction-row gl-gap-3" + :class="$options.FILTER_CSS_CLASSES" + > + <runner-filtered-search-bar + v-model="search" + :tokens="searchTokens" + :namespace="filteredSearchNamespace" + class="gl-flex-grow-1 gl-align-self-stretch" + /> + <runner-membership-toggle + v-if="shouldRenderAllAvailableToggle" + v-model="search.membership" + class="gl-align-self-end gl-md-align-self-center" + /> + </div> <runner-stats :scope="$options.GROUP_TYPE" :variables="countVariables" /> diff --git a/app/assets/javascripts/runner/runner_search_utils.js b/app/assets/javascripts/runner/runner_search_utils.js index dc582ccbac1..adc832b0600 100644 --- a/app/assets/javascripts/runner/runner_search_utils.js +++ b/app/assets/javascripts/runner/runner_search_utils.js @@ -13,10 +13,12 @@ import { PARAM_KEY_RUNNER_TYPE, PARAM_KEY_TAG, PARAM_KEY_SEARCH, + PARAM_KEY_MEMBERSHIP, PARAM_KEY_SORT, PARAM_KEY_AFTER, PARAM_KEY_BEFORE, DEFAULT_SORT, + DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE, } from './constants'; import { getPaginationVariables } from './utils'; @@ -57,9 +59,10 @@ import { getPaginationVariables } from './utils'; * @param {Object} search * @returns {boolean} True if the value follows the search format. */ -export const searchValidator = ({ runnerType, filters, sort }) => { +export const searchValidator = ({ runnerType, membership, filters, sort }) => { return ( (runnerType === null || typeof runnerType === 'string') && + (membership === null || typeof membership === 'string') && Array.isArray(filters) && typeof sort === 'string' ); @@ -140,9 +143,11 @@ export const updateOutdatedUrl = (url = window.location.href) => { export const fromUrlQueryToSearch = (query = window.location.search) => { const params = queryToObject(query, { gatherArrays: true }); const runnerType = params[PARAM_KEY_RUNNER_TYPE]?.[0] || null; + const membership = params[PARAM_KEY_MEMBERSHIP]?.[0] || null; return { runnerType, + membership: membership || DEFAULT_MEMBERSHIP, filters: prepareTokens( urlQueryToFilter(query, { filterNamesAllowList: [PARAM_KEY_PAUSED, PARAM_KEY_STATUS, PARAM_KEY_TAG], @@ -162,13 +167,14 @@ export const fromUrlQueryToSearch = (query = window.location.search) => { * @returns {String} New URL for the page */ export const fromSearchToUrl = ( - { runnerType = null, filters = [], sort = null, pagination = {} }, + { runnerType = null, membership = null, filters = [], sort = null, pagination = {} }, url = window.location.href, ) => { const filterParams = { // Defaults [PARAM_KEY_STATUS]: [], [PARAM_KEY_RUNNER_TYPE]: [], + [PARAM_KEY_MEMBERSHIP]: [], [PARAM_KEY_TAG]: [], // Current filters ...filterToQueryObject(processFilters(filters), { @@ -180,6 +186,10 @@ export const fromSearchToUrl = ( filterParams[PARAM_KEY_RUNNER_TYPE] = [runnerType]; } + if (membership && membership !== DEFAULT_MEMBERSHIP) { + filterParams[PARAM_KEY_MEMBERSHIP] = [membership]; + } + if (!filterParams[PARAM_KEY_SEARCH]) { filterParams[PARAM_KEY_SEARCH] = null; } @@ -203,6 +213,7 @@ export const fromSearchToUrl = ( */ export const fromSearchToVariables = ({ runnerType = null, + membership = null, filters = [], sort = null, pagination = {}, @@ -226,6 +237,9 @@ export const fromSearchToVariables = ({ if (runnerType) { filterVariables.type = runnerType; } + if (membership) { + filterVariables.membership = membership; + } if (sort) { filterVariables.sort = sort; } diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb index 2ae0442c005..f3c4244269d 100644 --- a/app/controllers/admin/groups_controller.rb +++ b/app/controllers/admin/groups_controller.rb @@ -60,17 +60,6 @@ class Admin::GroupsController < Admin::ApplicationController end end - def members_update - member_params = params.permit(:user_id, :access_level, :expires_at) - result = Members::CreateService.new(current_user, member_params.merge(limit: -1, source: @group, invite_source: 'admin-group-page')).execute - - if result[:status] == :success - redirect_to [:admin, @group], notice: _('Users were successfully added.') - else - redirect_to [:admin, @group], alert: result[:message] - end - end - def destroy Groups::DestroyService.new(@group, current_user).async_execute diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb index 0ca0fa729c0..4aa4256b044 100644 --- a/app/controllers/groups/runners_controller.rb +++ b/app/controllers/groups/runners_controller.rb @@ -5,6 +5,10 @@ class Groups::RunnersController < Groups::ApplicationController before_action :authorize_update_runner!, only: [:edit, :update, :destroy, :pause, :resume] before_action :runner, only: [:edit, :update, :destroy, :pause, :resume, :show] + before_action do + push_frontend_feature_flag(:runners_finder_all_available, @group) + end + before_action only: [:show] do push_frontend_feature_flag(:enforce_runner_token_expires_at) end diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb index 9ee0fd1db1d..ec0cf36d875 100644 --- a/app/models/concerns/cache_markdown_field.rb +++ b/app/models/concerns/cache_markdown_field.rb @@ -237,3 +237,5 @@ module CacheMarkdownField end end end + +CacheMarkdownField.prepend_mod diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml index a57d3170cbd..3a15a923c33 100644 --- a/app/views/admin/groups/show.html.haml +++ b/app/views/admin/groups/show.html.haml @@ -102,21 +102,6 @@ = render 'shared/admin/admin_note' - if can?(current_user, :admin_group_member, @group) - .card - .card-header - = _('Add user(s) to the group:') - .card-body.form-holder - %p.light - - help_link_open = '<strong><a href="%{help_url}">'.html_safe % { help_url: help_page_url("user/permissions") } - = html_escape(_('Read more about project permissions %{help_link_open}here%{help_link_close}')) % { help_link_open: help_link_open, help_link_close: '</a></strong>'.html_safe } - - = form_tag admin_group_members_update_path(@group), id: "new_project_member", class: "bulk_import", method: :put do - %div - = users_select_tag(:user_id, multiple: true, email_user: true, skip_ldap: @group.ldap_synced?, scope: :all) - .gl-mt-3 - = select_tag :access_level, options_for_select(@group.access_level_roles), class: "project-access-select select2" - %hr - = button_tag _('Add users to group'), class: "gl-button btn btn-confirm" = render 'shared/members/requests', membership_source: @group, group: @group, requesters: @requesters, force_mobile_view: true .card diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml index ef539029272..58108ceeb76 100644 --- a/app/views/shared/issuable/_milestone_dropdown.html.haml +++ b/app/views/shared/issuable/_milestone_dropdown.html.haml @@ -11,10 +11,6 @@ placeholder: _('Search milestones'), footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, show_started: show_started, field_name: name, selected: selected_text, project_id: project.try(:id), default_label: _('Milestone'), qa_selector: "issuable_milestone_dropdown", testid: "issuable-milestone-dropdown" } }) do - if project %ul.dropdown-footer-list - - if can? current_user, :admin_milestone, project - %li - = link_to new_project_milestone_path(project), title: _('New Milestone') do - = _('Create new') %li = link_to project_milestones_path(project) do - if can? current_user, :admin_milestone, project diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml index 98e2c6c43b1..31625c22a94 100644 --- a/app/views/shared/members/_requests.html.haml +++ b/app/views/shared/members/_requests.html.haml @@ -5,7 +5,7 @@ - return if requesters.empty? -= render Pajamas::CardComponent.new(card_options: { class: 'gl-mt-3 gl-mb-5', data: { testid: 'access-requests' } }, body_options: { class: 'gl-p-0' }) do |c| += render Pajamas::CardComponent.new(card_options: { class: 'gl-mb-5', data: { testid: 'access-requests' } }, body_options: { class: 'gl-p-0' }) do |c| - c.header do = _('Users requesting access to') %strong= membership_source.name diff --git a/config/routes.rb b/config/routes.rb index 704405bbcbd..f5e398aa986 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -70,11 +70,6 @@ InitializerConnections.with_disabled_database_connections do Gitlab.ee do resource :company, only: [:new, :create], controller: 'company' - - # legacy - to be removed with https://gitlab.com/gitlab-org/gitlab/-/issues/371996 - get 'groups/new', to: redirect('users/sign_up/groups_projects/new') - get 'projects/new', to: redirect('users/sign_up/groups_projects/new') - resources :groups_projects, only: [:new, :create] do collection do post :import diff --git a/doc/administration/geo/setup/external_database.md b/doc/administration/geo/setup/external_database.md index e4cd05a2b73..eabed7c10f3 100644 --- a/doc/administration/geo/setup/external_database.md +++ b/doc/administration/geo/setup/external_database.md @@ -69,8 +69,7 @@ To set up an external database, you can either: Given you have a primary site set up on AWS EC2 that uses RDS. You can now just create a read-only replica in a different region and the -replication process is managed by AWS. Make sure you've set Network ACL (Access Control List), Subnet, and -Security Group according to your needs, so the secondary Rails node(s) can access the database. +replication process is managed by AWS. Make sure you've set Network ACL (Access Control List), Subnet, and Security Group according to your needs, so the secondary Rails nodes can access the database. The following instructions detail how to create a read-only replica for common cloud providers: diff --git a/doc/architecture/blueprints/pods/index.md b/doc/architecture/blueprints/pods/index.md index de5c12c3c70..00f57a8cbac 100644 --- a/doc/architecture/blueprints/pods/index.md +++ b/doc/architecture/blueprints/pods/index.md @@ -143,7 +143,7 @@ We should likely position this as a push for GitLab workspaces and not talk abou What are other distinct advantages of workspaces that could be shipped? -- Easier admin controls +- Easier administrator controls - Better permission management - Instance-like UX diff --git a/doc/architecture/blueprints/rate_limiting/index.md b/doc/architecture/blueprints/rate_limiting/index.md index 692cef4b11d..2d2fe6ac511 100644 --- a/doc/architecture/blueprints/rate_limiting/index.md +++ b/doc/architecture/blueprints/rate_limiting/index.md @@ -357,7 +357,7 @@ hierarchy. Choosing a proper solution will require a thoughtful research. 1. Build application limits API in a way that it can be easily extracted to a separate service. 1. Build application limits definition in a way that is independent from the Rails application. 1. Build tooling that produce consistent behavior and results across programming languages. -1. Build the new framework in a way that we can extend to allow self-managed admins to customize limits. +1. Build the new framework in a way that we can extend to allow self-managed administrators to customize limits. 1. Maintain consistent features and behavior across SaaS and self-managed codebase. 1. Be mindful about a cognitive load added by the hierarchical limits, aim to reduce it. diff --git a/doc/development/audit_event_guide/index.md b/doc/development/audit_event_guide/index.md index 27a56d7904f..5c8938aa46a 100644 --- a/doc/development/audit_event_guide/index.md +++ b/doc/development/audit_event_guide/index.md @@ -19,7 +19,7 @@ actions performed across the application. While any events could trigger an Audit Event, not all events should. In general, events that are not good candidates for audit events are: - Not attributable to one specific user. -- Not of specific interest to an admin or owner persona. +- Not of specific interest to an administrator or owner persona. - Are tracking information for product feature adoption. - Are covered in the direction page's discussion on [what is not planned](https://about.gitlab.com/direction/manage/compliance/audit-events/#what-is-not-planned-right-now). diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md index 13cdf2c0c08..d3908e4e293 100644 --- a/doc/development/ee_features.md +++ b/doc/development/ee_features.md @@ -72,7 +72,7 @@ To guard your licensed feature: ``` 1. Optional. If your global feature is also available to namespaces with a paid plan, combine two -feature identifiers to allow both admins and group users. For example: +feature identifiers to allow both administrators and group users. For example: ```ruby License.feature_available?(:my_feature_name) || group.licensed_feature_available?(:my_feature_name_for_namespace) # Both admins and group members can see this EE feature diff --git a/doc/development/sidekiq/index.md b/doc/development/sidekiq/index.md index b94a9df8e32..547e740a4cf 100644 --- a/doc/development/sidekiq/index.md +++ b/doc/development/sidekiq/index.md @@ -186,3 +186,9 @@ default weight, which is 1. Each Sidekiq worker must be tested using RSpec, just like any other class. These tests should be placed in `spec/workers`. + +## Interacting with Sidekiq Redis + +The application should minimise interaction with of any `Sidekiq.redis`. Directly interacting with `Sidekiq.redis` in generic logic should be abstracted to a [Sidekiq middleware](https://gitlab.com/gitlab-org/gitlab/-/tree/master/lib/gitlab/sidekiq_middleware) for re-use across teams. By decoupling application logic from Sidekiq's datastore, it allows for greater freedom when horizontally scaling the GitLab background processing setup. + +Some exceptions to this rule would be migration-related logic or administration operations. diff --git a/doc/integration/arkose.md b/doc/integration/arkose.md index 84b30d69db4..aa27e3ba4a4 100644 --- a/doc/integration/arkose.md +++ b/doc/integration/arkose.md @@ -29,7 +29,7 @@ user doesn't need to take any additional action and can sign in as usual. ## How do we treat malicious sign-in attempts? Users are not denied access if Arkose Protect considers they are malicious. However, -their risk score is exposed in the admin console so that we can make more informed decisions when it +their risk score is exposed in the administrator console so that we can make more informed decisions when it comes to manually blocking users. When we decide to block a user, feedback is sent to ArkoseLabs to improve their risk prediction model. diff --git a/doc/integration/mattermost/index.md b/doc/integration/mattermost/index.md index 36a04f423a7..04b0157b737 100644 --- a/doc/integration/mattermost/index.md +++ b/doc/integration/mattermost/index.md @@ -272,7 +272,7 @@ There are 4 users on local instance ### Use `mmctl` through a remote connection For remote connections or local connections where the socket cannot be used, -create a non SSO user and give that user admin privileges. Those credentials +create a non SSO user and give that user administrator privileges. Those credentials can then be used to authenticate `mmctl`: ```shell diff --git a/doc/user/application_security/dast/checks/16.5.md b/doc/user/application_security/dast/checks/16.5.md index 9f9c3292ba4..285cc753523 100644 --- a/doc/user/application_security/dast/checks/16.5.md +++ b/doc/user/application_security/dast/checks/16.5.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Description -The target website returns AspNet header(s) and version information of this website. By +The target website returns AspNet headers and version information of this website. By exposing these values attackers may attempt to identify if the target software is vulnerable to known vulnerabilities, or catalog known sites running particular versions to exploit in the future when a vulnerability is identified in the particular version. diff --git a/doc/user/application_security/dast/checks/16.6.md b/doc/user/application_security/dast/checks/16.6.md index c6297d477fa..c6705b2ec7f 100644 --- a/doc/user/application_security/dast/checks/16.6.md +++ b/doc/user/application_security/dast/checks/16.6.md @@ -8,7 +8,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Description -The target website returns AspNet header(s) along with version information of this website. By +The target website returns AspNet headers along with version information of this website. By exposing these values attackers may attempt to identify if the target software is vulnerable to known vulnerabilities. Or catalog known sites running particular versions to exploit in the future when a vulnerability is identified in the particular version. diff --git a/doc/user/group/saml_sso/troubleshooting_scim.md b/doc/user/group/saml_sso/troubleshooting_scim.md index 2862d3c9125..f98b6c61e11 100644 --- a/doc/user/group/saml_sso/troubleshooting_scim.md +++ b/doc/user/group/saml_sso/troubleshooting_scim.md @@ -30,7 +30,7 @@ It is important that this SCIM `id` and SCIM `externalId` are configured to the ## How do I verify user's SAML NameId matches the SCIM externalId -Admins can use the Admin Area to [list SCIM identities for a user](../../admin_area/index.md#user-identities). +Administrators can use the Admin Area to [list SCIM identities for a user](../../admin_area/index.md#user-identities). Group owners can see the list of users and the `externalId` stored for each user in the group SAML SSO Settings page. diff --git a/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md b/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md index 04d8870cc22..b2b5da61a81 100644 --- a/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md +++ b/doc/user/infrastructure/clusters/migrate_to_gitlab_agent.md @@ -26,7 +26,7 @@ you can use the [CI/CD workflow](../../clusters/agent/ci_cd_workflow.md). This workflow uses an agent to connect to your cluster. The agent: - Is not exposed to the internet. -- Does not require full cluster-admin access to GitLab. +- Does not require full [`cluster-admin`](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#user-facing-roles) access to GitLab. NOTE: The certificate-based integration was used for popular GitLab features like diff --git a/doc/user/project/merge_requests/reviews/data_usage.md b/doc/user/project/merge_requests/reviews/data_usage.md index 460841838bc..60fc54facf3 100644 --- a/doc/user/project/merge_requests/reviews/data_usage.md +++ b/doc/user/project/merge_requests/reviews/data_usage.md @@ -9,35 +9,35 @@ type: index, reference ## How it works -Suggested Reviewers is the first user-facing GitLab machine learning (ML) powered feature. It leverages a project's contribution graph to generate suggestions. This data already exists within GitLab including merge request metadata, source code files, and GitLab user account metadata. +Suggested Reviewers is the first user-facing GitLab machine learning (ML) powered feature. It leverages a project's contribution graph to generate suggestions. This data already exists within GitLab including merge request metadata, source code files, and GitLab user account metadata. ### Enabling the feature When a Project Maintainer or Owner enables Suggested Reviewers in project settings GitLab kicks off a data extraction job for the project which leverages the Merge Request API to understand pattern of review including recency, domain experience, and frequency to suggest an appropriate reviewer. -This data extraction job can take a few hours to complete (possibly up to a day), which is largely dependent on the size of the project. The process is automated and no action is needed during this process. Once data extraction is complete, you will start getting suggestions in merge requests. +This data extraction job can take a few hours to complete (possibly up to a day), which is largely dependent on the size of the project. The process is automated and no action is needed during this process. Once data extraction is complete, you will start getting suggestions in merge requests. ### Generating suggestions -Once Suggested Reviewers is enabled and the data extraction is complete, new merge requests or new commits to existing merge requests will automatically trigger a Suggested Reviewers ML model inference and generate up to 5 suggested reviewers. These suggestions are contextual to the changes in the merge request. Additional commits to merge requests may change the reviewer suggestions which will automatically update in the reviewer dropdown. +Once Suggested Reviewers is enabled and the data extraction is complete, new merge requests or new commits to existing merge requests will automatically trigger a Suggested Reviewers ML model inference and generate up to 5 suggested reviewers. These suggestions are contextual to the changes in the merge request. Additional commits to merge requests may change the reviewer suggestions which will automatically update in the reviewer dropdown. ## Progressive enhancement -This feature is designed as a progressive enhancement to the existing GitLab Reviewers functionality. The GitLab Reviewer UI will only offer suggestions if the ML engine is able to provide a recommendation. In the event of an issue or model inference failure, the feature will gracefully degrade. At no point with the usage of Suggested Reviewers prevent a user from being able to manually set a reviewer. +This feature is designed as a progressive enhancement to the existing GitLab Reviewers functionality. The GitLab Reviewer UI will only offer suggestions if the ML engine is able to provide a recommendation. In the event of an issue or model inference failure, the feature will gracefully degrade. At no point with the usage of Suggested Reviewers prevent a user from being able to manually set a reviewer. ## Model Accuracy -Organizations use many different processes for code review. Some focus on senior engineers reviewing junior engineer's code, others have hierarchical organizational structure based reviews. Suggested Reviewers is focused on contextual reviewers based on historical merge request activity by users. While we will continue evolving the underlying ML model to better serve various code review use cases and processes Suggested Reviewers does not replace the usage of other code review features like Code Owners and [Approval Rules](../approvals/rules.md). Reviewer selection is highly subjective therefore, we do not expect Suggested Reviewers to provide perfect suggestions everytime. +Organizations use many different processes for code review. Some focus on senior engineers reviewing junior engineer's code, others have hierarchical organizational structure based reviews. Suggested Reviewers is focused on contextual reviewers based on historical merge request activity by users. While we will continue evolving the underlying ML model to better serve various code review use cases and processes Suggested Reviewers does not replace the usage of other code review features like Code Owners and [Approval Rules](../approvals/rules.md). Reviewer selection is highly subjective therefore, we do not expect Suggested Reviewers to provide perfect suggestions everytime. Through analysis of beta customer usage, we find that the Suggested Reviewers ML model provides suggestions that are adopted in 60% of cases. We will be introducing a feedback mechanism into the Suggested Reviewers feature in the future to allow users to flag bad reviewer suggestions to help improve the model. Additionally we will be offering an opt-in feature in the future which will allow the model to use your project's data for training the underlying model. ## Off by default -Suggested Reviewers is off by default and requires a Project Owner or Admin to enable the feature. +Suggested Reviewers is off by default and requires a Project Owner or Admin to enable the feature. ## Data privacy -Suggested Reviewers operates completely within the GitLab.com infrastructure providing the same level of [privacy](https://about.gitlab.com/privacy/) and [security](https://about.gitlab.com/security/) of any other feature of GitLab.com. +Suggested Reviewers operates completely within the GitLab.com infrastructure providing the same level of [privacy](https://about.gitlab.com/privacy/) and [security](https://about.gitlab.com/security/) of any other feature of GitLab.com. No new additional data is collected to enable this feature, simply GitLab is inferencing your merge request against a trained machine learning model. The content of your source code is not used as training data. Your data also never leaves GitLab.com, all training and inference is done within GitLab.com infrastructure. diff --git a/doc/user/tasks.md b/doc/user/tasks.md index df0d88a18f0..c8124229653 100644 --- a/doc/user/tasks.md +++ b/doc/user/tasks.md @@ -115,7 +115,7 @@ To change the assignee on a task: 1. In the issue description, in the **Tasks** section, select the title of the task you want to edit. The task window opens. 1. Next to **Assignees**, select **Add assignees**. -1. From the dropdown list, select the user(s) to add as an assignee. +1. From the dropdown list, select the users to add as an assignee. 1. Select any area outside the dropdown list. ## Set a start and due date diff --git a/doc/user/usage_quotas.md b/doc/user/usage_quotas.md index 2060a029833..e123f537424 100644 --- a/doc/user/usage_quotas.md +++ b/doc/user/usage_quotas.md @@ -13,7 +13,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w ## Namespace storage limit Namespaces on GitLab SaaS have a storage limit. For more information, see our [pricing page](https://about.gitlab.com/pricing/). -This limit is not visible on the Usage quotas page, but will be prior to [enforcement](#namespace-storage-limit-enforcement-schedule). Self-managed deployments are not affected. +This limit is not visible on the Usage quotas page, but will be prior to the limit being [applied](#namespace-storage-limit-application-schedule). Self-managed deployments are not affected. Storage types that add to the total namespace storage are: @@ -38,16 +38,14 @@ To prevent exceeding the namespace storage quota, you can: - [Start a trial](https://about.gitlab.com/free-trial/) or [upgrade to GitLab Premium or Ultimate](https://about.gitlab.com/pricing) which include higher limits and features that enable growing teams to ship faster without sacrificing on quality. - [Talk to an expert](https://page.gitlab.com/usage_limits_help.html) to learn more about your options and ask questions. -### Namespace storage limit enforcement schedule +### Namespace storage limit application schedule -Storage limits for GitLab SaaS Free tier namespaces will not be enforced prior to 2022-10-19. Storage limits for GitLab SaaS Paid tier namespaces will not be enforced for prior to 2023-02-15. Enforcement will not occur until all storage types are accurately measured, including deduplication of forks for [Git](https://gitlab.com/gitlab-org/gitlab/-/issues/371671) and [LFS](https://gitlab.com/gitlab-org/gitlab/-/issues/370242). +Information on when namespace-level storage limits will be applied is available on these FAQ pages for the [Free](https://about.gitlab.com/pricing/faq-efficient-free-tier/#storage-limits-on-gitlab-saas-free-tier) and [Paid](https://about.gitlab.com/pricing/faq-paid-storage-transfer/) tier. -Impacted users are notified by email and through in-app notifications at least 60 days prior to enforcement. - -### Project storage limit +## Project storage limit Projects on GitLab SaaS have a 10GB storage limit on their Git repository and LFS storage. -After namespace-level storage limits are enforced, the project limit is removed. A namespace has either a namespace-level storage limit or a project-level storage limit, but not both. +After namespace-level storage limits are applied, the project limit will be removed. A namespace has either a namespace-level storage limit or a project-level storage limit, but not both. When a project's repository and LFS reaches the quota, the project is locked. You cannot push changes to a locked project. To monitor the size of each @@ -102,7 +100,7 @@ Depending on your role, you can also use the following methods to manage or redu ## Excess storage usage -Excess storage usage is the amount that a project's repository and LFS exceeds the free storage quota. If no +Excess storage usage is the amount that a project's repository and LFS exceeds the [project storage limit](#project-storage-limit). If no purchased storage is available the project is locked. You cannot push changes to a locked project. To unlock a project you must [purchase more storage](../subscriptions/gitlab_com/index.md#purchase-more-storage-and-transfer) for the namespace. When the purchase is completed, locked projects are automatically unlocked. The diff --git a/lib/gitlab/redis/duplicate_jobs.rb b/lib/gitlab/redis/duplicate_jobs.rb index beb3ba1abee..c76d298da18 100644 --- a/lib/gitlab/redis/duplicate_jobs.rb +++ b/lib/gitlab/redis/duplicate_jobs.rb @@ -18,9 +18,11 @@ module Gitlab # `Sidekiq.redis` is a namespaced redis connection. This means keys are actually being stored under # "resque:gitlab:resque:gitlab:duplicate:". For backwards compatibility, we make the secondary store # namespaced in the same way, but omit it from the primary so keys have proper format there. + # rubocop:disable Cop/RedisQueueUsage secondary_store = ::Redis::Namespace.new( Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE, redis: ::Redis.new(Gitlab::Redis::Queues.params) ) + # rubocop:enable Cop/RedisQueueUsage MultiStore.new(primary_store, secondary_store, name.demodulize) end diff --git a/lib/gitlab/redis/sidekiq_status.rb b/lib/gitlab/redis/sidekiq_status.rb index d4362c7cad8..9b8bbf5a0ad 100644 --- a/lib/gitlab/redis/sidekiq_status.rb +++ b/lib/gitlab/redis/sidekiq_status.rb @@ -14,7 +14,7 @@ module Gitlab def redis primary_store = ::Redis.new(Gitlab::Redis::SharedState.params) - secondary_store = ::Redis.new(Gitlab::Redis::Queues.params) + secondary_store = ::Redis.new(Gitlab::Redis::Queues.params) # rubocop:disable Cop/RedisQueueUsage MultiStore.new(primary_store, secondary_store, name.demodulize) end diff --git a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb index ab126ea4749..d42bd672bac 100644 --- a/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb +++ b/lib/gitlab/sidekiq_middleware/duplicate_jobs/duplicate_job.rb @@ -282,7 +282,7 @@ module Gitlab Gitlab::Redis::DuplicateJobs.with { |redis| yield redis } else # Keep the old behavior intact if neither feature flag is turned on - Sidekiq.redis { |redis| yield redis } + Sidekiq.redis { |redis| yield redis } # rubocop:disable Cop/SidekiqRedisCall end end end diff --git a/lib/gitlab/sidekiq_status.rb b/lib/gitlab/sidekiq_status.rb index 9d08d236720..17234bdf519 100644 --- a/lib/gitlab/sidekiq_status.rb +++ b/lib/gitlab/sidekiq_status.rb @@ -126,7 +126,7 @@ module Gitlab Gitlab::Redis::SidekiqStatus.with { |redis| yield redis } else # Keep the old behavior intact if neither feature flag is turned on - Sidekiq.redis { |redis| yield redis } + Sidekiq.redis { |redis| yield redis } # rubocop:disable Cop/SidekiqRedisCall end end private_class_method :with_redis diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 536c5f376b1..604d065267f 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -2384,12 +2384,6 @@ msgstr "" msgid "Add trigger" msgstr "" -msgid "Add user(s) to the group:" -msgstr "" - -msgid "Add users to group" -msgstr "" - msgid "Add variable" msgstr "" @@ -17355,6 +17349,9 @@ msgstr "" msgid "Geo|Errors:" msgstr "" +msgid "Geo|External URL" +msgstr "" + msgid "Geo|Failed" msgstr "" @@ -32889,9 +32886,6 @@ msgstr "" msgid "Read more about GitLab at %{link_to_promo}." msgstr "" -msgid "Read more about project permissions %{help_link_open}here%{help_link_close}" -msgstr "" - msgid "Read more about related epics" msgstr "" @@ -34779,6 +34773,9 @@ msgstr "" msgid "Runners|Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console." msgstr "" +msgid "Runners|Show only inherited" +msgstr "" + msgid "Runners|Show runner installation and registration instructions" msgstr "" @@ -43745,9 +43742,6 @@ msgstr "" msgid "Users to exclude from the rate limit" msgstr "" -msgid "Users were successfully added." -msgstr "" - msgid "Users with a Guest role or those who don't belong to a Project or Group will not use a seat from your license." msgstr "" diff --git a/rubocop/cop/redis_queue_usage.rb b/rubocop/cop/redis_queue_usage.rb new file mode 100644 index 00000000000..d993abc6327 --- /dev/null +++ b/rubocop/cop/redis_queue_usage.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # This class complements Rubocop::Cop::SidekiqRedisCall by disallowing the use of + # Gitlab::Redis::Queues with the exception of initialising Sidekiq and monitoring. + class RedisQueueUsage < RuboCop::Cop::Base + MSG = 'Gitlab::Redis::Queues should only be used by Sidekiq initializers. '\ + 'Assignments or using its params to initialise another connection is not allowed.' + + def_node_matcher :calling_redis_queue_module_methods?, <<~PATTERN + (send (const (const (const nil? :Gitlab) :Redis) :Queues) ...) + PATTERN + + def_node_matcher :using_redis_queue_module_as_parameter?, <<~PATTERN + (send ... (const (const (const nil? :Gitlab) :Redis) :Queues)) + PATTERN + + def_node_matcher :redis_queue_assignment?, <<~PATTERN + ({lvasgn | ivasgn | cvasgn | gvasgn | casgn | masgn | op_asgn | or_asgn | and_asgn } ... + `(const (const (const nil? :Gitlab) :Redis) :Queues)) + PATTERN + + def on_send(node) + return unless using_redis_queue_module_as_parameter?(node) || calling_redis_queue_module_methods?(node) + + add_offense(node, message: MSG) + end + + # offenses caught in assignment may overlap with on_send + %i[on_lvasgn on_ivasgn on_cvasgn on_gvasgn on_casgn on_masgn on_op_asgn on_or_asgn on_and_asgn].each do |name| + define_method(name) do |node| + add_offense(node, message: MSG) if redis_queue_assignment?(node) + end + end + end + end +end diff --git a/rubocop/cop/sidekiq_redis_call.rb b/rubocop/cop/sidekiq_redis_call.rb new file mode 100644 index 00000000000..e4ae430f7c7 --- /dev/null +++ b/rubocop/cop/sidekiq_redis_call.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module RuboCop + module Cop + # Cop that prevents manually setting a queue in Sidekiq workers. + class SidekiqRedisCall < RuboCop::Cop::Base + MSG = 'Refrain from directly using Sidekiq.redis unless for migration. For admin operations, use Sidekiq APIs.' + + def_node_matcher :using_sidekiq_redis?, <<~PATTERN + (send (const nil? :Sidekiq) :redis) + PATTERN + + def on_send(node) + add_offense(node, message: MSG) if using_sidekiq_redis?(node) + end + end + end +end diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb index fb843ac6a7a..37cb0a1f289 100644 --- a/spec/controllers/admin/groups_controller_spec.rb +++ b/spec/controllers/admin/groups_controller_spec.rb @@ -44,64 +44,4 @@ RSpec.describe Admin::GroupsController do end.to change { Namespace::AdminNote.count }.by(1) end end - - describe 'PUT #members_update' do - let_it_be(:group_user) { create(:user) } - - it 'adds user to members', :aggregate_failures, :snowplow do - put :members_update, params: { - id: group, - user_id: group_user.id, - access_level: Gitlab::Access::GUEST - } - - expect(controller).to set_flash.to 'Users were successfully added.' - expect(response).to redirect_to(admin_group_path(group)) - expect(group.users).to include group_user - expect_snowplow_event( - category: 'Members::CreateService', - action: 'create_member', - label: 'admin-group-page', - property: 'existing_user', - user: admin - ) - end - - it 'can add unlimited members', :aggregate_failures do - put :members_update, params: { - id: group, - user_id: 1.upto(1000).to_a.join(','), - access_level: Gitlab::Access::GUEST - } - - expect(controller).to set_flash.to 'Users were successfully added.' - expect(response).to redirect_to(admin_group_path(group)) - end - - it 'adds no user to members', :aggregate_failures do - put :members_update, params: { - id: group, - user_id: '', - access_level: Gitlab::Access::GUEST - } - - expect(controller).to set_flash.to 'No users specified.' - expect(response).to redirect_to(admin_group_path(group)) - expect(group.users).not_to include group_user - end - - it 'updates the project_creation_level successfully' do - expect do - post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS } } - end.to change { group.reload.project_creation_level }.to(::Gitlab::Access::NO_ONE_PROJECT_ACCESS) - end - - it 'updates the subgroup_creation_level successfully' do - expect do - post :update, - params: { id: group.to_param, - group: { subgroup_creation_level: ::Gitlab::Access::OWNER_SUBGROUP_ACCESS } } - end.to change { group.reload.subgroup_creation_level }.to(::Gitlab::Access::OWNER_SUBGROUP_ACCESS) - end - end end diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb index 040c6a65b7c..657dd52228e 100644 --- a/spec/features/admin/admin_groups_spec.rb +++ b/spec/features/admin/admin_groups_spec.rb @@ -207,31 +207,6 @@ RSpec.describe 'Admin Groups' do end describe 'add user into a group', :js do - shared_examples 'adds user into a group' do - it do - visit admin_group_path(group) - - select2(user_selector, from: '#user_id', multiple: true) - page.within '#new_project_member' do - select2(Gitlab::Access::REPORTER, from: '#access_level') - end - click_button "Add users to group" - - page.within ".group-users-list" do - expect(page).to have_content(user.name) - expect(page).to have_content('Reporter') - end - end - end - - it_behaves_like 'adds user into a group' do - let(:user_selector) { user.id } - end - - it_behaves_like 'adds user into a group' do - let(:user_selector) { user.email } - end - context 'when membership is set to expire' do it 'renders relative time' do expire_time = Time.current + 2.days diff --git a/spec/features/admin/admin_settings_spec.rb b/spec/features/admin/admin_settings_spec.rb index a5df142d188..b2e86112d98 100644 --- a/spec/features/admin/admin_settings_spec.rb +++ b/spec/features/admin/admin_settings_spec.rb @@ -7,7 +7,7 @@ RSpec.describe 'Admin updates settings' do include TermsHelper include UsageDataHelpers - let(:admin) { create(:admin) } + let_it_be(:admin) { create(:admin) } let(:dot_com?) { false } context 'application setting :admin_mode is enabled', :request_store do diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js index 8cfaba6f98a..8953e3cbcd8 100644 --- a/spec/frontend/labels/components/promote_label_modal_spec.js +++ b/spec/frontend/labels/components/promote_label_modal_spec.js @@ -1,98 +1,100 @@ -import Vue from 'vue'; +import { shallowMount } from '@vue/test-utils'; +import { GlModal, GlSprintf } from '@gitlab/ui'; +import AxiosMockAdapter from 'axios-mock-adapter'; + import { TEST_HOST } from 'helpers/test_constants'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { stubComponent } from 'helpers/stub_component'; + import axios from '~/lib/utils/axios_utils'; -import promoteLabelModal from '~/labels/components/promote_label_modal.vue'; +import PromoteLabelModal from '~/labels/components/promote_label_modal.vue'; import eventHub from '~/labels/event_hub'; describe('Promote label modal', () => { - let vm; - const Component = Vue.extend(promoteLabelModal); + let wrapper; + let axiosMock; + const labelMockData = { labelTitle: 'Documentation', - labelColor: '#5cb85c', - labelTextColor: '#ffffff', + labelColor: 'rgb(92, 184, 92)', + labelTextColor: 'rgb(255, 255, 255)', url: `${TEST_HOST}/dummy/promote/labels`, groupName: 'group', }; - describe('Modal title and description', () => { - beforeEach(() => { - vm = mountComponent(Component, labelMockData); + const createComponent = () => { + wrapper = shallowMount(PromoteLabelModal, { + propsData: labelMockData, + stubs: { + GlSprintf, + GlModal: stubComponent(GlModal, { + template: `<div><slot name="modal-title"></slot><slot></slot></div>`, + }), + }, }); + }; - afterEach(() => { - vm.$destroy(); - }); + beforeEach(() => { + axiosMock = new AxiosMockAdapter(axios); + createComponent(); + }); + afterEach(() => { + axiosMock.reset(); + wrapper.destroy(); + }); + + describe('Modal title and description', () => { it('contains the proper description', () => { - expect(vm.text).toContain( + expect(wrapper.text()).toContain( `Promoting ${labelMockData.labelTitle} will make it available for all projects inside ${labelMockData.groupName}`, ); }); it('contains a label span with the color', () => { - expect(vm.labelColor).not.toBe(null); - expect(vm.labelColor).toBe(labelMockData.labelColor); - expect(vm.labelTitle).toBe(labelMockData.labelTitle); + const label = wrapper.find('.modal-title-with-label .label'); + + expect(label.element.style.backgroundColor).toBe(labelMockData.labelColor); + expect(label.element.style.color).toBe(labelMockData.labelTextColor); + expect(label.text()).toBe(labelMockData.labelTitle); }); }); describe('When requesting a label promotion', () => { beforeEach(() => { - vm = mountComponent(Component, { - ...labelMockData, - }); jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); }); - afterEach(() => { - vm.$destroy(); - }); - - it('redirects when a label is promoted', () => { + it('redirects when a label is promoted', async () => { const responseURL = `${TEST_HOST}/dummy/endpoint`; - jest.spyOn(axios, 'post').mockImplementation((url) => { - expect(url).toBe(labelMockData.url); - expect(eventHub.$emit).toHaveBeenCalledWith( - 'promoteLabelModal.requestStarted', - labelMockData.url, - ); - return Promise.resolve({ - request: { - responseURL, - }, - }); - }); + axiosMock.onPost(labelMockData.url).reply(200, { url: responseURL }); - return vm.onSubmit().then(() => { - expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { - labelUrl: labelMockData.url, - successful: true, - }); + wrapper.findComponent(GlModal).vm.$emit('primary'); + + expect(eventHub.$emit).toHaveBeenCalledWith( + 'promoteLabelModal.requestStarted', + labelMockData.url, + ); + + await axios.waitForAll(); + + expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { + labelUrl: labelMockData.url, + successful: true, }); }); - it('displays an error if promoting a label failed', () => { + it('displays an error if promoting a label failed', async () => { const dummyError = new Error('promoting label failed'); dummyError.response = { status: 500 }; + axiosMock.onPost(labelMockData.url).reply(500, { error: dummyError }); - jest.spyOn(axios, 'post').mockImplementation((url) => { - expect(url).toBe(labelMockData.url); - expect(eventHub.$emit).toHaveBeenCalledWith( - 'promoteLabelModal.requestStarted', - labelMockData.url, - ); + wrapper.findComponent(GlModal).vm.$emit('primary'); - return Promise.reject(dummyError); - }); + await axios.waitForAll(); - return vm.onSubmit().catch((error) => { - expect(error).toBe(dummyError); - expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { - labelUrl: labelMockData.url, - successful: false, - }); + expect(eventHub.$emit).toHaveBeenCalledWith('promoteLabelModal.requestFinished', { + labelUrl: labelMockData.url, + successful: false, }); }); }); diff --git a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js index ebf21c01324..17669331370 100644 --- a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js +++ b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js @@ -1,9 +1,10 @@ -import Vue from 'vue'; +import Vue, { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; +import { GlModal } from '@gitlab/ui'; import { TEST_HOST } from 'helpers/test_constants'; -import mountComponent from 'helpers/vue_mount_component_helper'; import axios from '~/lib/utils/axios_utils'; import { redirectTo } from '~/lib/utils/url_utility'; -import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; +import StopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue'; jest.mock('~/lib/utils/url_utility', () => ({ ...jest.requireActual('~/lib/utils/url_utility'), @@ -14,20 +15,23 @@ describe('stop_jobs_modal.vue', () => { const props = { url: `${TEST_HOST}/stop_jobs_modal.vue/stopAll`, }; - let vm; + let wrapper; - afterEach(() => { - vm.$destroy(); + beforeEach(() => { + wrapper = mount(StopJobsModal, { propsData: props }); }); - beforeEach(() => { - const Component = Vue.extend(stopJobsModal); - vm = mountComponent(Component, props); + afterEach(() => { + wrapper.destroy(); }); - describe('onSubmit', () => { + describe('on submit', () => { it('stops jobs and redirects to overview page', async () => { const responseURL = `${TEST_HOST}/stop_jobs_modal.vue/jobs`; + // TODO: We can't use axios-mock-adapter because our current version + // does not support responseURL + // + // see https://gitlab.com/gitlab-org/gitlab/-/issues/375308 for details jest.spyOn(axios, 'post').mockImplementation((url) => { expect(url).toBe(props.url); return Promise.resolve({ @@ -37,18 +41,28 @@ describe('stop_jobs_modal.vue', () => { }); }); - await vm.onSubmit(); + wrapper.findComponent(GlModal).vm.$emit('primary'); + await nextTick(); + expect(redirectTo).toHaveBeenCalledWith(responseURL); }); it('displays error if stopping jobs failed', async () => { + Vue.config.errorHandler = () => {}; // silencing thrown error + const dummyError = new Error('stopping jobs failed'); + // TODO: We can't use axios-mock-adapter because our current version + // does not support responseURL + // + // see https://gitlab.com/gitlab-org/gitlab/-/issues/375308 for details jest.spyOn(axios, 'post').mockImplementation((url) => { expect(url).toBe(props.url); return Promise.reject(dummyError); }); - await expect(vm.onSubmit()).rejects.toEqual(dummyError); + wrapper.findComponent(GlModal).vm.$emit('primary'); + await nextTick(); + expect(redirectTo).not.toHaveBeenCalled(); }); }); diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js index 07a7f1bb2ff..608516385d9 100644 --- a/spec/frontend/pdf/page_spec.js +++ b/spec/frontend/pdf/page_spec.js @@ -1,5 +1,5 @@ -import Vue, { nextTick } from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; import PageComponent from '~/pdf/page/index.vue'; jest.mock('pdfjs-dist/webpack', () => { @@ -7,11 +7,10 @@ jest.mock('pdfjs-dist/webpack', () => { }); describe('Page component', () => { - const Component = Vue.extend(PageComponent); - let vm; + let wrapper; afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); it('renders the page when mounting', async () => { @@ -20,16 +19,18 @@ describe('Page component', () => { getViewport: jest.fn().mockReturnValue({}), }; - vm = mountComponent(Component, { - page: testPage, - number: 1, + wrapper = mount(PageComponent, { + propsData: { + page: testPage, + number: 1, + }, }); - expect(vm.rendering).toBe(true); - await nextTick(); - expect(testPage.render).toHaveBeenCalledWith(vm.renderContext); - expect(vm.rendering).toBe(false); + expect(testPage.render).toHaveBeenCalledWith({ + canvasContext: wrapper.find('canvas').element.getContext('2d'), + viewport: testPage.getViewport(), + }); }); }); diff --git a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js index c1c9daab2a5..8a9ea77f6fd 100644 --- a/spec/frontend/runner/admin_runners/admin_runners_app_spec.js +++ b/spec/frontend/runner/admin_runners/admin_runners_app_spec.js @@ -45,6 +45,7 @@ import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, + DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE, } from '~/runner/constants'; import allRunnersQuery from 'ee_else_ce/runner/graphql/list/all_runners.query.graphql'; @@ -221,6 +222,7 @@ describe('AdminRunnersApp', () => { expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: undefined, type: undefined, + membership: DEFAULT_MEMBERSHIP, sort: DEFAULT_SORT, first: RUNNER_PAGE_SIZE, }); @@ -290,6 +292,7 @@ describe('AdminRunnersApp', () => { it('sets the filters in the search bar', () => { expect(findRunnerFilteredSearchBar().props('value')).toEqual({ runnerType: INSTANCE_TYPE, + membership: DEFAULT_MEMBERSHIP, filters: [ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, { type: PARAM_KEY_PAUSED, value: { data: 'true', operator: '=' } }, @@ -303,6 +306,7 @@ describe('AdminRunnersApp', () => { expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, type: INSTANCE_TYPE, + membership: DEFAULT_MEMBERSHIP, paused: true, sort: DEFAULT_SORT, first: RUNNER_PAGE_SIZE, @@ -312,6 +316,7 @@ describe('AdminRunnersApp', () => { it('fetches count results for requested status', () => { expect(mockRunnersCountHandler).toHaveBeenCalledWith({ type: INSTANCE_TYPE, + membership: DEFAULT_MEMBERSHIP, status: STATUS_ONLINE, paused: true, }); @@ -324,6 +329,7 @@ describe('AdminRunnersApp', () => { findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], sort: CREATED_ASC, }); @@ -341,6 +347,7 @@ describe('AdminRunnersApp', () => { it('requests the runners with filters', () => { expect(mockRunnersHandler).toHaveBeenLastCalledWith({ status: STATUS_ONLINE, + membership: DEFAULT_MEMBERSHIP, sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); @@ -349,6 +356,7 @@ describe('AdminRunnersApp', () => { it('fetches count results for requested status', () => { expect(mockRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_ONLINE, + membership: DEFAULT_MEMBERSHIP, }); }); }); @@ -459,6 +467,7 @@ describe('AdminRunnersApp', () => { beforeEach(async () => { findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], sort: CREATED_ASC, }); @@ -506,6 +515,7 @@ describe('AdminRunnersApp', () => { await findRunnerPaginationNext().trigger('click'); expect(mockRunnersHandler).toHaveBeenLastCalledWith({ + membership: DEFAULT_MEMBERSHIP, sort: CREATED_DESC, first: RUNNER_PAGE_SIZE, after: pageInfo.endCursor, diff --git a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js index e35bec3aa38..c92e19f9263 100644 --- a/spec/frontend/runner/components/runner_filtered_search_bar_spec.js +++ b/spec/frontend/runner/components/runner_filtered_search_bar_spec.js @@ -4,10 +4,26 @@ import RunnerFilteredSearchBar from '~/runner/components/runner_filtered_search_ import { statusTokenConfig } from '~/runner/components/search_tokens/status_token_config'; import TagToken from '~/runner/components/search_tokens/tag_token.vue'; import { tagTokenConfig } from '~/runner/components/search_tokens/tag_token_config'; -import { PARAM_KEY_STATUS, PARAM_KEY_TAG, STATUS_ONLINE, INSTANCE_TYPE } from '~/runner/constants'; +import { + PARAM_KEY_STATUS, + PARAM_KEY_TAG, + STATUS_ONLINE, + INSTANCE_TYPE, + DEFAULT_MEMBERSHIP, + DEFAULT_SORT, + CONTACTED_DESC, +} from '~/runner/constants'; import FilteredSearch from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue'; import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue'; +const mockSearch = { + runnerType: null, + membership: DEFAULT_MEMBERSHIP, + filters: [], + pagination: { page: 1 }, + sort: DEFAULT_SORT, +}; + describe('RunnerList', () => { let wrapper; @@ -15,8 +31,7 @@ describe('RunnerList', () => { const findGlFilteredSearch = () => wrapper.findComponent(GlFilteredSearch); const findSortOptions = () => wrapper.findAllComponents(GlDropdownItem); - const mockDefaultSort = 'CREATED_DESC'; - const mockOtherSort = 'CONTACTED_DESC'; + const mockOtherSort = CONTACTED_DESC; const mockFilters = [ { type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }, { type: 'filtered-search-term', value: { data: '' } }, @@ -32,11 +47,7 @@ describe('RunnerList', () => { propsData: { namespace: 'runners', tokens: [], - value: { - runnerType: null, - filters: [], - sort: mockDefaultSort, - }, + value: mockSearch, ...props, }, stubs: { @@ -115,6 +126,7 @@ describe('RunnerList', () => { props: { value: { runnerType: INSTANCE_TYPE, + membership: DEFAULT_MEMBERSHIP, sort: mockOtherSort, filters: mockFilters, }, @@ -141,6 +153,7 @@ describe('RunnerList', () => { expectToHaveLastEmittedInput({ runnerType: INSTANCE_TYPE, + membership: DEFAULT_MEMBERSHIP, filters: mockFilters, sort: mockOtherSort, pagination: {}, @@ -154,8 +167,9 @@ describe('RunnerList', () => { expectToHaveLastEmittedInput({ runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: mockFilters, - sort: mockDefaultSort, + sort: DEFAULT_SORT, pagination: {}, }); }); @@ -165,6 +179,7 @@ describe('RunnerList', () => { expectToHaveLastEmittedInput({ runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [], sort: mockOtherSort, pagination: {}, diff --git a/spec/frontend/runner/components/runner_membership_toggle_spec.js b/spec/frontend/runner/components/runner_membership_toggle_spec.js new file mode 100644 index 00000000000..1a7ae22618a --- /dev/null +++ b/spec/frontend/runner/components/runner_membership_toggle_spec.js @@ -0,0 +1,57 @@ +import { GlToggle } from '@gitlab/ui'; +import { shallowMount, mount } from '@vue/test-utils'; +import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue'; +import { + I18N_SHOW_ONLY_INHERITED, + MEMBERSHIP_DESCENDANTS, + MEMBERSHIP_ALL_AVAILABLE, +} from '~/runner/constants'; + +describe('RunnerMembershipToggle', () => { + let wrapper; + + const findToggle = () => wrapper.findComponent(GlToggle); + + const createComponent = ({ props = {}, mountFn = shallowMount } = {}) => { + wrapper = mountFn(RunnerMembershipToggle, { + propsData: props, + }); + }; + + afterEach(() => { + wrapper.destroy(); + }); + + it('Displays text', () => { + createComponent({ mountFn: mount }); + + expect(wrapper.text()).toBe(I18N_SHOW_ONLY_INHERITED); + }); + + it.each` + membershipValue | toggleValue + ${MEMBERSHIP_DESCENDANTS} | ${true} + ${MEMBERSHIP_ALL_AVAILABLE} | ${false} + `( + 'Displays a membership of $membershipValue as enabled=$toggleValue', + ({ membershipValue, toggleValue }) => { + createComponent({ props: { value: membershipValue } }); + + expect(findToggle().props('value')).toBe(toggleValue); + }, + ); + + it.each` + changeEvt | membershipValue + ${true} | ${MEMBERSHIP_DESCENDANTS} + ${false} | ${MEMBERSHIP_ALL_AVAILABLE} + `( + 'Emits $changeEvt when value is changed to $membershipValue', + ({ changeEvt, membershipValue }) => { + createComponent(); + findToggle().vm.$emit('change', changeEvt); + + expect(wrapper.emitted('input')).toStrictEqual([[membershipValue]]); + }, + ); +}); diff --git a/spec/frontend/runner/components/runner_type_tabs_spec.js b/spec/frontend/runner/components/runner_type_tabs_spec.js index 1577d3b1f72..dde35533bc3 100644 --- a/spec/frontend/runner/components/runner_type_tabs_spec.js +++ b/spec/frontend/runner/components/runner_type_tabs_spec.js @@ -2,9 +2,21 @@ import { GlTab } from '@gitlab/ui'; import { shallowMount } from '@vue/test-utils'; import RunnerTypeTabs from '~/runner/components/runner_type_tabs.vue'; import RunnerCount from '~/runner/components/stat/runner_count.vue'; -import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/runner/constants'; - -const mockSearch = { runnerType: null, filters: [], pagination: { page: 1 }, sort: 'CREATED_DESC' }; +import { + INSTANCE_TYPE, + GROUP_TYPE, + PROJECT_TYPE, + DEFAULT_MEMBERSHIP, + DEFAULT_SORT, +} from '~/runner/constants'; + +const mockSearch = { + runnerType: null, + membership: DEFAULT_MEMBERSHIP, + filters: [], + pagination: { page: 1 }, + sort: DEFAULT_SORT, +}; const mockCount = (type, multiplier = 1) => { let count; diff --git a/spec/frontend/runner/group_runners/group_runners_app_spec.js b/spec/frontend/runner/group_runners/group_runners_app_spec.js index a17502c7eec..d61fb1229b4 100644 --- a/spec/frontend/runner/group_runners/group_runners_app_spec.js +++ b/spec/frontend/runner/group_runners/group_runners_app_spec.js @@ -24,6 +24,7 @@ import RunnerStats from '~/runner/components/stat/runner_stats.vue'; import RunnerActionsCell from '~/runner/components/cells/runner_actions_cell.vue'; import RegistrationDropdown from '~/runner/components/registration/registration_dropdown.vue'; import RunnerPagination from '~/runner/components/runner_pagination.vue'; +import RunnerMembershipToggle from '~/runner/components/runner_membership_toggle.vue'; import { CREATED_ASC, @@ -39,6 +40,8 @@ import { STATUS_ONLINE, STATUS_OFFLINE, STATUS_STALE, + MEMBERSHIP_ALL_AVAILABLE, + MEMBERSHIP_DESCENDANTS, RUNNER_PAGE_SIZE, I18N_EDIT, } from '~/runner/constants'; @@ -89,8 +92,14 @@ describe('GroupRunnersApp', () => { const findRunnerPagination = () => extendedWrapper(wrapper.findComponent(RunnerPagination)); const findRunnerPaginationNext = () => findRunnerPagination().findByText(s__('Pagination|Next')); const findRunnerFilteredSearchBar = () => wrapper.findComponent(RunnerFilteredSearchBar); - - const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => { + const findRunnerMembershipToggle = () => wrapper.findComponent(RunnerMembershipToggle); + + const createComponent = ({ + props = {}, + provide = {}, + mountFn = shallowMountExtended, + ...options + } = {}) => { const handlers = [ [groupRunnersQuery, mockGroupRunnersHandler], [groupRunnersCountQuery, mockGroupRunnersCountHandler], @@ -109,6 +118,7 @@ describe('GroupRunnersApp', () => { staleTimeoutSecs, emptyStateSvgPath, emptyStateFilteredSvgPath, + ...provide, }, ...options, }); @@ -147,19 +157,75 @@ describe('GroupRunnersApp', () => { expect(findRegistrationDropdown().props('type')).toBe(GROUP_TYPE); }); + describe('show all available runners toggle', () => { + describe('when runners_finder_all_available is enabled', () => { + it('shows the membership toggle', () => { + createComponent({ + provide: { + glFeatures: { runnersFinderAllAvailable: true }, + }, + }); + + expect(findRunnerMembershipToggle().exists()).toBe(true); + }); + + it('sets the membership toggle', () => { + setWindowLocation(`?membership[]=${MEMBERSHIP_ALL_AVAILABLE}`); + createComponent({ + provide: { + glFeatures: { runnersFinderAllAvailable: true }, + }, + }); + + expect(findRunnerMembershipToggle().props('value')).toBe(MEMBERSHIP_ALL_AVAILABLE); + }); + + it('requests filter', async () => { + createComponent({ + provide: { + glFeatures: { runnersFinderAllAvailable: true }, + }, + }); + + findRunnerMembershipToggle().vm.$emit('input', MEMBERSHIP_ALL_AVAILABLE); + + await waitForPromises(); + + expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith( + expect.objectContaining({ + membership: MEMBERSHIP_ALL_AVAILABLE, + }), + ); + }); + }); + + describe('when runners_finder_all_available is disabled', () => { + beforeEach(() => { + createComponent(); + }); + + it('does not show the membership toggle', () => { + expect(findRunnerMembershipToggle().exists()).toBe(false); + }); + }); + }); + it('shows total runner counts', async () => { await createComponent({ mountFn: mountExtended }); expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_ONLINE, + membership: MEMBERSHIP_DESCENDANTS, groupFullPath: mockGroupFullPath, }); expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_OFFLINE, + membership: MEMBERSHIP_DESCENDANTS, groupFullPath: mockGroupFullPath, }); expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ status: STATUS_STALE, + membership: MEMBERSHIP_DESCENDANTS, groupFullPath: mockGroupFullPath, }); @@ -183,6 +249,7 @@ describe('GroupRunnersApp', () => { groupFullPath: mockGroupFullPath, status: undefined, type: undefined, + membership: MEMBERSHIP_DESCENDANTS, sort: DEFAULT_SORT, first: RUNNER_PAGE_SIZE, }); @@ -266,6 +333,7 @@ describe('GroupRunnersApp', () => { it('sets the filters in the search bar', () => { expect(findRunnerFilteredSearchBar().props('value')).toEqual({ runnerType: INSTANCE_TYPE, + membership: MEMBERSHIP_DESCENDANTS, filters: [{ type: 'status', value: { data: STATUS_ONLINE, operator: '=' } }], sort: 'CREATED_DESC', pagination: {}, @@ -277,6 +345,7 @@ describe('GroupRunnersApp', () => { groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, type: INSTANCE_TYPE, + membership: MEMBERSHIP_DESCENDANTS, sort: DEFAULT_SORT, first: RUNNER_PAGE_SIZE, }); @@ -286,6 +355,7 @@ describe('GroupRunnersApp', () => { expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ groupFullPath: mockGroupFullPath, type: INSTANCE_TYPE, + membership: MEMBERSHIP_DESCENDANTS, status: STATUS_ONLINE, }); }); @@ -297,6 +367,7 @@ describe('GroupRunnersApp', () => { findRunnerFilteredSearchBar().vm.$emit('input', { runnerType: null, + membership: MEMBERSHIP_DESCENDANTS, filters: [{ type: PARAM_KEY_STATUS, value: { data: STATUS_ONLINE, operator: '=' } }], sort: CREATED_ASC, }); @@ -315,6 +386,7 @@ describe('GroupRunnersApp', () => { expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, + membership: MEMBERSHIP_DESCENDANTS, sort: CREATED_ASC, first: RUNNER_PAGE_SIZE, }); @@ -324,6 +396,7 @@ describe('GroupRunnersApp', () => { expect(mockGroupRunnersCountHandler).toHaveBeenCalledWith({ groupFullPath: mockGroupFullPath, status: STATUS_ONLINE, + membership: MEMBERSHIP_DESCENDANTS, }); }); }); @@ -395,6 +468,7 @@ describe('GroupRunnersApp', () => { expect(mockGroupRunnersHandler).toHaveBeenLastCalledWith({ groupFullPath: mockGroupFullPath, + membership: MEMBERSHIP_DESCENDANTS, sort: CREATED_DESC, first: RUNNER_PAGE_SIZE, after: pageInfo.endCursor, diff --git a/spec/frontend/runner/mock_data.js b/spec/frontend/runner/mock_data.js index 555ec40184f..da0c0433b3e 100644 --- a/spec/frontend/runner/mock_data.js +++ b/spec/frontend/runner/mock_data.js @@ -17,7 +17,7 @@ import groupRunnersData from 'test_fixtures/graphql/runner/list/group_runners.qu import groupRunnersDataPaginated from 'test_fixtures/graphql/runner/list/group_runners.query.graphql.paginated.json'; import groupRunnersCountData from 'test_fixtures/graphql/runner/list/group_runners_count.query.graphql.json'; -import { RUNNER_PAGE_SIZE } from '~/runner/constants'; +import { DEFAULT_MEMBERSHIP, RUNNER_PAGE_SIZE } from '~/runner/constants'; const emptyPageInfo = { __typename: 'PageInfo', @@ -34,8 +34,18 @@ export const mockSearchExamples = [ { name: 'a default query', urlQuery: '', - search: { runnerType: null, filters: [], pagination: {}, sort: 'CREATED_DESC' }, - graphqlVariables: { sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + search: { + runnerType: null, + membership: DEFAULT_MEMBERSHIP, + filters: [], + pagination: {}, + sort: 'CREATED_DESC', + }, + graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, isDefault: true, }, { @@ -43,17 +53,24 @@ export const mockSearchExamples = [ urlQuery: '?status[]=ACTIVE', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], pagination: {}, sort: 'CREATED_DESC', }, - graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, + status: 'ACTIVE', + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, }, { name: 'a single term text search', urlQuery: '?search=something', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [ { type: 'filtered-search-term', @@ -63,13 +80,19 @@ export const mockSearchExamples = [ pagination: {}, sort: 'CREATED_DESC', }, - graphqlVariables: { search: 'something', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, + search: 'something', + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, }, { name: 'a two terms text search', urlQuery: '?search=something+else', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [ { type: 'filtered-search-term', @@ -83,24 +106,36 @@ export const mockSearchExamples = [ pagination: {}, sort: 'CREATED_DESC', }, - graphqlVariables: { search: 'something else', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, + search: 'something else', + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, }, { name: 'single instance type', urlQuery: '?runner_type[]=INSTANCE_TYPE', search: { runnerType: 'INSTANCE_TYPE', + membership: DEFAULT_MEMBERSHIP, filters: [], pagination: {}, sort: 'CREATED_DESC', }, - graphqlVariables: { type: 'INSTANCE_TYPE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + type: 'INSTANCE_TYPE', + membership: DEFAULT_MEMBERSHIP, + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, }, { name: 'multiple runner status', urlQuery: '?status[]=ACTIVE&status[]=PAUSED', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [ { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, { type: 'status', value: { data: 'PAUSED', operator: '=' } }, @@ -108,13 +143,19 @@ export const mockSearchExamples = [ pagination: {}, sort: 'CREATED_DESC', }, - graphqlVariables: { status: 'ACTIVE', sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + status: 'ACTIVE', + membership: DEFAULT_MEMBERSHIP, + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, }, { name: 'multiple status, a single instance type and a non default sort', urlQuery: '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&sort=CREATED_ASC', search: { runnerType: 'INSTANCE_TYPE', + membership: DEFAULT_MEMBERSHIP, filters: [{ type: 'status', value: { data: 'ACTIVE', operator: '=' } }], pagination: {}, sort: 'CREATED_ASC', @@ -122,6 +163,7 @@ export const mockSearchExamples = [ graphqlVariables: { status: 'ACTIVE', type: 'INSTANCE_TYPE', + membership: DEFAULT_MEMBERSHIP, sort: 'CREATED_ASC', first: RUNNER_PAGE_SIZE, }, @@ -131,11 +173,13 @@ export const mockSearchExamples = [ urlQuery: '?tag[]=tag-1', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [{ type: 'tag', value: { data: 'tag-1', operator: '=' } }], pagination: {}, sort: 'CREATED_DESC', }, graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, tagList: ['tag-1'], first: 20, sort: 'CREATED_DESC', @@ -146,6 +190,7 @@ export const mockSearchExamples = [ urlQuery: '?tag[]=tag-1&tag[]=tag-2', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [ { type: 'tag', value: { data: 'tag-1', operator: '=' } }, { type: 'tag', value: { data: 'tag-2', operator: '=' } }, @@ -154,6 +199,7 @@ export const mockSearchExamples = [ sort: 'CREATED_DESC', }, graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, tagList: ['tag-1', 'tag-2'], first: 20, sort: 'CREATED_DESC', @@ -164,22 +210,34 @@ export const mockSearchExamples = [ urlQuery: '?after=AFTER_CURSOR', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [], pagination: { after: 'AFTER_CURSOR' }, sort: 'CREATED_DESC', }, - graphqlVariables: { sort: 'CREATED_DESC', after: 'AFTER_CURSOR', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, + sort: 'CREATED_DESC', + after: 'AFTER_CURSOR', + first: RUNNER_PAGE_SIZE, + }, }, { name: 'the previous page', urlQuery: '?before=BEFORE_CURSOR', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [], pagination: { before: 'BEFORE_CURSOR' }, sort: 'CREATED_DESC', }, - graphqlVariables: { sort: 'CREATED_DESC', before: 'BEFORE_CURSOR', last: RUNNER_PAGE_SIZE }, + graphqlVariables: { + membership: DEFAULT_MEMBERSHIP, + sort: 'CREATED_DESC', + before: 'BEFORE_CURSOR', + last: RUNNER_PAGE_SIZE, + }, }, { name: 'the next page filtered by a status, an instance type, tags and a non default sort', @@ -187,6 +245,7 @@ export const mockSearchExamples = [ '?status[]=ACTIVE&runner_type[]=INSTANCE_TYPE&tag[]=tag-1&tag[]=tag-2&sort=CREATED_ASC&after=AFTER_CURSOR', search: { runnerType: 'INSTANCE_TYPE', + membership: DEFAULT_MEMBERSHIP, filters: [ { type: 'status', value: { data: 'ACTIVE', operator: '=' } }, { type: 'tag', value: { data: 'tag-1', operator: '=' } }, @@ -198,6 +257,7 @@ export const mockSearchExamples = [ graphqlVariables: { status: 'ACTIVE', type: 'INSTANCE_TYPE', + membership: DEFAULT_MEMBERSHIP, tagList: ['tag-1', 'tag-2'], sort: 'CREATED_ASC', after: 'AFTER_CURSOR', @@ -209,22 +269,34 @@ export const mockSearchExamples = [ urlQuery: '?paused[]=true', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [{ type: 'paused', value: { data: 'true', operator: '=' } }], pagination: {}, sort: 'CREATED_DESC', }, - graphqlVariables: { paused: true, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + paused: true, + membership: DEFAULT_MEMBERSHIP, + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, }, { name: 'active runners', urlQuery: '?paused[]=false', search: { runnerType: null, + membership: DEFAULT_MEMBERSHIP, filters: [{ type: 'paused', value: { data: 'false', operator: '=' } }], pagination: {}, sort: 'CREATED_DESC', }, - graphqlVariables: { paused: false, sort: 'CREATED_DESC', first: RUNNER_PAGE_SIZE }, + graphqlVariables: { + paused: false, + membership: DEFAULT_MEMBERSHIP, + sort: 'CREATED_DESC', + first: RUNNER_PAGE_SIZE, + }, }, ]; diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js index 2606933450e..a3aa563b516 100644 --- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js +++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merged_spec.js @@ -1,180 +1,172 @@ import { getByRole } from '@testing-library/dom'; -import Vue from 'vue'; -import mountComponent from 'helpers/vue_mount_component_helper'; +import { nextTick } from 'vue'; +import { mount } from '@vue/test-utils'; import waitForPromises from 'helpers/wait_for_promises'; import { OPEN_REVERT_MODAL, OPEN_CHERRY_PICK_MODAL } from '~/projects/commit/constants'; import modalEventHub from '~/projects/commit/event_hub'; -import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue'; +import MergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue'; import eventHub from '~/vue_merge_request_widget/event_hub'; describe('MRWidgetMerged', () => { - let vm; + let wrapper; const targetBranch = 'foo'; - - beforeEach(() => { - jest.spyOn(document, 'dispatchEvent'); - const Component = Vue.extend(mergedComponent); - const mr = { - isRemovingSourceBranch: false, - cherryPickInForkPath: false, - canCherryPickInCurrentMR: true, - revertInForkPath: false, - canRevertInCurrentMR: true, - canRemoveSourceBranch: true, - sourceBranchRemoved: true, - metrics: { - mergedBy: { - name: 'Administrator', - username: 'root', - webUrl: 'http://localhost:3000/root', - avatarUrl: - 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + const mr = { + isRemovingSourceBranch: false, + cherryPickInForkPath: false, + canCherryPickInCurrentMR: true, + revertInForkPath: false, + canRevertInCurrentMR: true, + canRemoveSourceBranch: true, + sourceBranchRemoved: true, + metrics: { + mergedBy: { + name: 'Administrator', + username: 'root', + webUrl: 'http://localhost:3000/root', + avatarUrl: + 'http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon', + }, + mergedAt: 'Jan 24, 2018 1:02pm UTC', + readableMergedAt: '', + closedBy: {}, + closedAt: 'Jan 24, 2018 1:02pm UTC', + readableClosedAt: '', + }, + updatedAt: 'mergedUpdatedAt', + shortMergeCommitSha: '958c0475', + mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed', + mergeCommitPath: + 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d', + sourceBranch: 'bar', + targetBranch, + }; + + const service = { + removeSourceBranch: () => nextTick(), + }; + + const createComponent = (customMrFields = {}) => { + wrapper = mount(MergedComponent, { + propsData: { + mr: { + ...mr, + ...customMrFields, }, - mergedAt: 'Jan 24, 2018 1:02pm UTC', - readableMergedAt: '', - closedBy: {}, - closedAt: 'Jan 24, 2018 1:02pm UTC', - readableClosedAt: '', + service, }, - updatedAt: 'mergedUpdatedAt', - shortMergeCommitSha: '958c0475', - mergeCommitSha: '958c047516e182dfc52317f721f696e8a1ee85ed', - mergeCommitPath: - 'http://localhost:3000/root/nautilus/commit/f7ce827c314c9340b075657fd61c789fb01cf74d', - sourceBranch: 'bar', - targetBranch, - }; - - const service = { - removeSourceBranch() {}, - }; + }); + }; + beforeEach(() => { + jest.spyOn(document, 'dispatchEvent'); jest.spyOn(eventHub, '$emit').mockImplementation(() => {}); - - vm = mountComponent(Component, { mr, service }); }); afterEach(() => { - vm.$destroy(); + wrapper.destroy(); }); - describe('computed', () => { - describe('shouldShowRemoveSourceBranch', () => { - it('returns true when sourceBranchRemoved is false', () => { - vm.mr.sourceBranchRemoved = false; - - expect(vm.shouldShowRemoveSourceBranch).toEqual(true); - }); - - it('returns false when sourceBranchRemoved is true', () => { - vm.mr.sourceBranchRemoved = true; - - expect(vm.shouldShowRemoveSourceBranch).toEqual(false); - }); - - it('returns false when canRemoveSourceBranch is false', () => { - vm.mr.sourceBranchRemoved = false; - vm.mr.canRemoveSourceBranch = false; - - expect(vm.shouldShowRemoveSourceBranch).toEqual(false); - }); - - it('returns false when is making request', () => { - vm.mr.canRemoveSourceBranch = true; - vm.isMakingRequest = true; - - expect(vm.shouldShowRemoveSourceBranch).toEqual(false); - }); + const findButtonByText = (text) => + wrapper.findAll('button').wrappers.find((w) => w.text() === text); + const findRemoveSourceBranchButton = () => findButtonByText('Delete source branch'); - it('returns true when all are true', () => { - vm.mr.isRemovingSourceBranch = true; - vm.mr.canRemoveSourceBranch = true; - vm.isMakingRequest = true; + describe('remove source branch button', () => { + it('is displayed when sourceBranchRemoved is false', () => { + createComponent({ sourceBranchRemoved: false }); - expect(vm.shouldShowRemoveSourceBranch).toEqual(false); - }); + expect(findRemoveSourceBranchButton().exists()).toBe(true); }); - describe('shouldShowSourceBranchRemoving', () => { - it('should correct value when fields changed', () => { - vm.mr.sourceBranchRemoved = false; + it('is not displayed when sourceBranchRemoved is true', () => { + createComponent({ sourceBranchRemoved: true }); - expect(vm.shouldShowSourceBranchRemoving).toEqual(false); + expect(findRemoveSourceBranchButton()).toBe(undefined); + }); - vm.mr.sourceBranchRemoved = true; + it('is not displayed when canRemoveSourceBranch is true', () => { + createComponent({ sourceBranchRemoved: false, canRemoveSourceBranch: false }); - expect(vm.shouldShowRemoveSourceBranch).toEqual(false); + expect(findRemoveSourceBranchButton()).toBe(undefined); + }); - vm.mr.sourceBranchRemoved = false; - vm.isMakingRequest = true; + it('is not displayed when is making request', async () => { + createComponent({ sourceBranchRemoved: false, canRemoveSourceBranch: true }); - expect(vm.shouldShowSourceBranchRemoving).toEqual(true); + await findRemoveSourceBranchButton().trigger('click'); - vm.isMakingRequest = false; - vm.mr.isRemovingSourceBranch = true; + expect(findRemoveSourceBranchButton()).toBe(undefined); + }); - expect(vm.shouldShowSourceBranchRemoving).toEqual(true); + it('is not displayed when all are true', () => { + createComponent({ + isRemovingSourceBranch: true, + sourceBranchRemoved: false, + canRemoveSourceBranch: true, }); + + expect(findRemoveSourceBranchButton()).toBe(undefined); }); }); - describe('methods', () => { - describe('removeSourceBranch', () => { - it('should set flag and call service then request main component to update the widget', async () => { - jest.spyOn(vm.service, 'removeSourceBranch').mockReturnValue( - new Promise((resolve) => { - resolve({ - data: { - message: 'Branch was deleted', - }, - }); - }), - ); + it('should set flag and call service then request main component to update the widget when branch is removed', async () => { + createComponent({ sourceBranchRemoved: false }); + jest.spyOn(service, 'removeSourceBranch').mockResolvedValue({ + data: { + message: 'Branch was deleted', + }, + }); - vm.removeSourceBranch(); + await findRemoveSourceBranchButton().trigger('click'); - await waitForPromises(); + await waitForPromises(); - const args = eventHub.$emit.mock.calls[0]; + const args = eventHub.$emit.mock.calls[0]; - expect(vm.isMakingRequest).toEqual(true); - expect(args[0]).toEqual('MRWidgetUpdateRequested'); - expect(args[1]).not.toThrow(); - }); - }); + expect(args[0]).toEqual('MRWidgetUpdateRequested'); + expect(args[1]).not.toThrow(); }); it('calls dispatchDocumentEvent to load in the modal component', () => { + createComponent(); + expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('merged:UpdateActions')); }); it('emits event to open the revert modal on revert button click', () => { + createComponent(); const eventHubSpy = jest.spyOn(modalEventHub, '$emit'); - getByRole(vm.$el, 'button', { name: /Revert/i }).click(); + getByRole(wrapper.element, 'button', { name: /Revert/i }).click(); expect(eventHubSpy).toHaveBeenCalledWith(OPEN_REVERT_MODAL); }); it('emits event to open the cherry-pick modal on cherry-pick button click', () => { + createComponent(); const eventHubSpy = jest.spyOn(modalEventHub, '$emit'); - getByRole(vm.$el, 'button', { name: /Cherry-pick/i }).click(); + getByRole(wrapper.element, 'button', { name: /Cherry-pick/i }).click(); expect(eventHubSpy).toHaveBeenCalledWith(OPEN_CHERRY_PICK_MODAL); }); it('has merged by information', () => { - expect(vm.$el.textContent).toContain('Merged by'); - expect(vm.$el.textContent).toContain('Administrator'); + createComponent(); + + expect(wrapper.text()).toContain('Merged by'); + expect(wrapper.text()).toContain('Administrator'); }); it('shows revert and cherry-pick buttons', () => { - expect(vm.$el.textContent).toContain('Revert'); - expect(vm.$el.textContent).toContain('Cherry-pick'); + createComponent(); + + expect(wrapper.text()).toContain('Revert'); + expect(wrapper.text()).toContain('Cherry-pick'); }); it('should use mergedEvent mergedAt as tooltip title', () => { - expect(vm.$el.querySelector('time').getAttribute('title')).toBe('Jan 24, 2018 1:02pm UTC'); + createComponent(); + + expect(wrapper.find('time').attributes('title')).toBe('Jan 24, 2018 1:02pm UTC'); }); }); diff --git a/spec/rubocop/cop/redis_queue_usage_spec.rb b/spec/rubocop/cop/redis_queue_usage_spec.rb new file mode 100644 index 00000000000..9861a6a79d9 --- /dev/null +++ b/spec/rubocop/cop/redis_queue_usage_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../../rubocop/cop/redis_queue_usage' + +RSpec.describe RuboCop::Cop::RedisQueueUsage do + let(:msg) { described_class::MSG } + + context 'when assigning Gitlab::Redis::Queues as a variable' do + it 'registers offence for any variable assignment' do + expect_offense(<<~PATTERN) + x = Gitlab::Redis::Queues + ^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for constant assignment' do + expect_offense(<<~PATTERN) + X = Gitlab::Redis::Queues + ^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + end + + context 'when assigning Gitlab::Redis::Queues as a part of an array' do + it 'registers offence for variable assignments' do + expect_offense(<<~PATTERN) + x = [ Gitlab::Redis::Cache, Gitlab::Redis::Queues, Gitlab::Redis::SharedState ] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for constant assignments' do + expect_offense(<<~PATTERN) + ALL = [ Gitlab::Redis::Cache, Gitlab::Redis::Queues, Gitlab::Redis::SharedState ] + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for constant assignments while invoking function' do + expect_offense(<<~PATTERN) + ALL = [ Gitlab::Redis::Cache, Gitlab::Redis::Queues, Gitlab::Redis::SharedState ].freeze + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for constant assignments while invoking multiple functions' do + expect_offense(<<~PATTERN) + ALL = [ Gitlab::Redis::Cache, Gitlab::Redis::Queues, Gitlab::Redis::SharedState ].foo.freeze + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + end + + context 'when assigning Gitlab::Redis::Queues as a part of a hash' do + it 'registers offence for variable assignments' do + expect_offense(<<~PATTERN) + x = { "test": Gitlab::Redis::Queues, "test2": Gitlab::Redis::SharedState } + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for constant assignments' do + expect_offense(<<~PATTERN) + ALL = { "test": Gitlab::Redis::Queues, "test2": Gitlab::Redis::SharedState } + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for constant assignments while invoking function' do + expect_offense(<<~PATTERN) + ALL = { "test": Gitlab::Redis::Queues, "test2": Gitlab::Redis::SharedState }.freeze + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for constant assignments while invoking multiple functions' do + expect_offense(<<~PATTERN) + ALL = { "test": Gitlab::Redis::Queues, "test2": Gitlab::Redis::SharedState }.foo.freeze + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + end + + it 'registers offence for any invocation of Gitlab::Redis::Queues methods' do + expect_offense(<<~PATTERN) + Gitlab::Redis::Queues.params + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end + + it 'registers offence for using Gitlab::Redis::Queues as parameter in method calls' do + expect_offense(<<~PATTERN) + use_redis(Gitlab::Redis::Queues) + ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{msg} + PATTERN + end +end diff --git a/spec/rubocop/cop/sidekiq_redis_call_spec.rb b/spec/rubocop/cop/sidekiq_redis_call_spec.rb new file mode 100644 index 00000000000..7d1c68bfabe --- /dev/null +++ b/spec/rubocop/cop/sidekiq_redis_call_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require 'rubocop_spec_helper' + +require_relative '../../../rubocop/cop/sidekiq_redis_call' + +RSpec.describe RuboCop::Cop::SidekiqRedisCall do + it 'flags any use of Sidekiq.redis even without blocks' do + expect_offense(<<~PATTERN) + Sidekiq.redis + ^^^^^^^^^^^^^ Refrain from directly using Sidekiq.redis unless for migration. For admin operations, use Sidekiq APIs. + PATTERN + end + + it 'flags the use of Sidekiq.redis in single-line blocks' do + expect_offense(<<~PATTERN) + Sidekiq.redis { |redis| yield redis } + ^^^^^^^^^^^^^ Refrain from directly using Sidekiq.redis unless for migration. For admin operations, use Sidekiq APIs. + PATTERN + end + + it 'flags the use of Sidekiq.redis in multi-line blocks' do + expect_offense(<<~PATTERN) + Sidekiq.redis do |conn| + ^^^^^^^^^^^^^ Refrain from directly using Sidekiq.redis unless for migration. For admin operations, use Sidekiq APIs. + conn.sadd('queues', queues) + end + PATTERN + end +end diff --git a/workhorse/go.mod b/workhorse/go.mod index 5fa9ca7a52d..b933692f997 100644 --- a/workhorse/go.mod +++ b/workhorse/go.mod @@ -7,7 +7,7 @@ require ( github.com/BurntSushi/toml v1.2.0 github.com/FZambia/sentinel v1.1.1 github.com/alecthomas/chroma/v2 v2.3.0 - github.com/aws/aws-sdk-go v1.44.107 + github.com/aws/aws-sdk-go v1.44.109 github.com/disintegration/imaging v1.6.2 github.com/getsentry/raven-go v0.2.0 github.com/golang-jwt/jwt/v4 v4.4.2 diff --git a/workhorse/go.sum b/workhorse/go.sum index 3275e43115e..7e2996a7148 100644 --- a/workhorse/go.sum +++ b/workhorse/go.sum @@ -179,8 +179,8 @@ github.com/aws/aws-sdk-go v1.15.27/go.mod h1:mFuSZ37Z9YOHbQEwBWztmVzqXrEkub65tZo github.com/aws/aws-sdk-go v1.17.4/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= github.com/aws/aws-sdk-go v1.37.0/go.mod h1:hcU610XS61/+aQV88ixoOzUoG7v3b31pl2zKMmprdro= github.com/aws/aws-sdk-go v1.43.31/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= -github.com/aws/aws-sdk-go v1.44.107 h1:VP7Rq3wzsOV7wrfHqjAAKRksD4We58PaoVSDPKhm8nw= -github.com/aws/aws-sdk-go v1.44.107/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= +github.com/aws/aws-sdk-go v1.44.109 h1:+Na5JPeS0kiEHoBp5Umcuuf+IDqXqD0lXnM920E31YI= +github.com/aws/aws-sdk-go v1.44.109/go.mod h1:y4AeaBuwd2Lk+GepC1E9v0qOiTws0MIWAX4oIKwKHZo= github.com/aws/aws-sdk-go-v2 v1.16.2 h1:fqlCk6Iy3bnCumtrLz9r3mJ/2gUT0pJ0wLFVIdWh+JA= github.com/aws/aws-sdk-go-v2 v1.16.2/go.mod h1:ytwTPBG6fXTZLxxeeCCWj2/EMYp/xDUgX+OET6TLNNU= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.4.1 h1:SdK4Ppk5IzLs64ZMvr6MrSficMtjY2oS0WOORXTlxwU= |