diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-23 21:09:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-04-23 21:09:46 +0000 |
commit | f6b349ed51e973c4a01d2f1a85459816f478186c (patch) | |
tree | 46d49a2a1c1347ce8f3abe2ef1c9f9c4ba05cacd | |
parent | bc62085601fa730985ea84f88a96c39d870c6ea6 (diff) | |
download | gitlab-ce-f6b349ed51e973c4a01d2f1a85459816f478186c.tar.gz |
Add latest changes from gitlab-org/gitlab@master
36 files changed, 564 insertions, 87 deletions
diff --git a/.haml-lint.yml b/.haml-lint.yml index 622a70d047c..1d1c0fa1de2 100644 --- a/.haml-lint.yml +++ b/.haml-lint.yml @@ -110,7 +110,6 @@ linters: - Layout/EmptyLineAfterGuardClause - Layout/LeadingCommentSpace - Layout/SpaceAroundOperators - - Layout/SpaceBeforeBlockBraces - Layout/SpaceBeforeComma - Layout/SpaceBeforeFirstArg - Layout/SpaceInsideHashLiteralBraces diff --git a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue index 273d8d972f7..fcc900bbc96 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/actions.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/actions.vue @@ -76,8 +76,9 @@ export default { :value="$options.commitToCurrentBranch" :disabled="!canPushToBranch" :title="$options.currentBranchPermissionsTooltip" + data-qa-selector="commit_to_current_branch_radio_container" > - <span class="ide-option-label" data-qa-selector="commit_to_current_branch_radio"> + <span class="ide-option-label"> <gl-sprintf :message="s__('IDE|Commit to %{branchName} branch')"> <template #branchName> <strong class="monospace">{{ currentBranchText }}</strong> diff --git a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue index 039b4a54b26..870355e884e 100644 --- a/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue +++ b/app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue @@ -64,6 +64,7 @@ export default { :disabled="disabled" type="radio" name="commit-action" + data-qa-selector="commit_type_radio" @change="updateCommitAction($event.target.value)" /> <span class="gl-ml-3"> diff --git a/app/models/user.rb b/app/models/user.rb index 5fc7cc5574e..482f028a06b 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -337,6 +337,8 @@ class User < ApplicationRecord end event :deactivate do + # Any additional changes to this event should be also + # reflected in app/workers/users/deactivate_dormant_users_worker.rb transition active: :deactivated end @@ -418,6 +420,8 @@ class User < ApplicationRecord scope :order_recent_last_activity, -> { reorder(Gitlab::Database.nulls_last_order('last_activity_on', 'DESC')) } scope :order_oldest_last_activity, -> { reorder(Gitlab::Database.nulls_first_order('last_activity_on', 'ASC')) } scope :by_id_and_login, ->(id, login) { where(id: id).where('username = LOWER(:login) OR email = LOWER(:login)', login: login) } + scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) } + scope :with_no_activity, -> { active.where(last_activity_on: nil) } def preferred_language read_attribute('preferred_language') || diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index a38615d9b1b..359e5b411b1 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -5,7 +5,7 @@ .col-sm-6 .bs-callout %p - = (_"Runners are processes that pick up and execute CI/CD jobs for GitLab.") + = _("Runners are processes that pick up and execute CI/CD jobs for GitLab.") %br = _('You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want.') %br diff --git a/app/views/projects/_merge_request_merge_method_settings.html.haml b/app/views/projects/_merge_request_merge_method_settings.html.haml index f55d840e14b..2d18285ba80 100644 --- a/app/views/projects/_merge_request_merge_method_settings.html.haml +++ b/app/views/projects/_merge_request_merge_method_settings.html.haml @@ -22,7 +22,7 @@ = s_('ProjectSettings|When there is a merge conflict, the user is given the option to rebase.') .form-check.mb-2 - = form.radio_button :merge_method, :ff, class: "js-merge-method-radio form-check-input", data: { qa_selector: 'merge_ff_radio_button' } + = form.radio_button :merge_method, :ff, class: "js-merge-method-radio form-check-input", data: { qa_selector: 'merge_ff_radio' } = label_tag :project_merge_method_ff, class: 'form-check-label' do = s_('ProjectSettings|Fast-forward merge') .text-secondary diff --git a/app/views/shared/_group_form.html.haml b/app/views/shared/_group_form.html.haml index eea0c5f37de..7055dc8142a 100644 --- a/app/views/shared/_group_form.html.haml +++ b/app/views/shared/_group_form.html.haml @@ -6,7 +6,7 @@ .form-group.group-name-holder.col-sm-12 = f.label :name, class: 'label-bold' do = _("Group name") - = f.text_field :name, placeholder: _('My Awesome Group'), class: 'js-autofill-group-name form-control input-lg', + = f.text_field :name, placeholder: _('My Awesome Group'), class: 'js-autofill-group-name form-control input-lg', data: { qa_selector: 'group_name_field' }, required: true, title: _('Please fill in a descriptive name for your group.'), autofocus: true @@ -22,7 +22,7 @@ - if parent %strong= parent.full_path + '/' = f.hidden_field :parent_id - = f.text_field :path, placeholder: _('my-awesome-group'), class: 'form-control js-validate-group-path js-autofill-group-path', + = f.text_field :path, placeholder: _('my-awesome-group'), class: 'form-control js-validate-group-path js-autofill-group-path', data: { qa_selector: 'group_path_field' }, autofocus: local_assigns[:autofocus] || false, required: true, pattern: Gitlab::PathRegex::NAMESPACE_FORMAT_REGEX_JS, title: _('Please choose a group URL with no special characters.'), diff --git a/app/views/shared/tokens/_scopes_form.html.haml b/app/views/shared/tokens/_scopes_form.html.haml index 1c8e300fa8a..33e95446bd7 100644 --- a/app/views/shared/tokens/_scopes_form.html.haml +++ b/app/views/shared/tokens/_scopes_form.html.haml @@ -4,6 +4,6 @@ - scopes.each do |scope| %fieldset.form-group.form-check - = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input qa-#{scope}-radio" + = check_box_tag "#{prefix}[scopes][]", scope, token.scopes.include?(scope), id: "#{prefix}_scopes_#{scope}", class: "form-check-input", data: { qa_selector: "#{scope}_checkbox" } = label_tag "#{prefix}_scopes_#{scope}", scope, class: 'label-bold form-check-label' .text-secondary= t scope, scope: scope_description(prefix) diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index fa6ea54e342..0417d214231 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -515,6 +515,14 @@ :weight: 1 :idempotent: :tags: [] +- :name: cronjob:users_deactivate_dormant_users + :feature_category: :utilization + :has_external_dependencies: + :urgency: :low + :resource_boundary: :unknown + :weight: 1 + :idempotent: + :tags: [] - :name: cronjob:x509_issuer_crl_check :feature_category: :source_code_management :has_external_dependencies: true diff --git a/app/workers/database/batched_background_migration_worker.rb b/app/workers/database/batched_background_migration_worker.rb index de274d58ad7..039b45a497f 100644 --- a/app/workers/database/batched_background_migration_worker.rb +++ b/app/workers/database/batched_background_migration_worker.rb @@ -38,7 +38,7 @@ module Database end def with_exclusive_lease(interval) - timeout = max(interval * LEASE_TIMEOUT_MULTIPLIER, MINIMUM_LEASE_TIMEOUT) + timeout = [interval * LEASE_TIMEOUT_MULTIPLIER, MINIMUM_LEASE_TIMEOUT].max lease = Gitlab::ExclusiveLease.new(lease_key, timeout: timeout) yield if lease.try_obtain @@ -46,10 +46,6 @@ module Database lease&.cancel end - def max(left, right) - left >= right ? left : right - end - def lease_key self.class.name.demodulize.underscore end diff --git a/app/workers/users/deactivate_dormant_users_worker.rb b/app/workers/users/deactivate_dormant_users_worker.rb new file mode 100644 index 00000000000..f92a12310b6 --- /dev/null +++ b/app/workers/users/deactivate_dormant_users_worker.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Users + class DeactivateDormantUsersWorker # rubocop:disable Scalability/IdempotentWorker + include ApplicationWorker + include CronjobQueue + + feature_category :utilization + + NUMBER_OF_BATCHES = 50 + BATCH_SIZE = 200 + PAUSE_SECONDS = 0.25 + + def perform + return if Gitlab.com? + + return unless ::Gitlab::CurrentSettings.current_application_settings.deactivate_dormant_users + + with_context(caller_id: self.class.name.to_s) do + NUMBER_OF_BATCHES.times do + result = User.connection.execute(update_query) + + break if result.cmd_tuples == 0 + + sleep(PAUSE_SECONDS) + end + end + end + + private + + def update_query + <<~SQL + UPDATE "users" + SET "state" = 'deactivated' + WHERE "users"."id" IN ( + (#{users.dormant.to_sql}) + UNION + (#{users.with_no_activity.to_sql}) + LIMIT #{BATCH_SIZE} + ) + SQL + end + + def users + User.select(:id).limit(BATCH_SIZE) + end + end +end diff --git a/changelogs/unreleased/211754-automate-dormant-users-deactivation.yml b/changelogs/unreleased/211754-automate-dormant-users-deactivation.yml new file mode 100644 index 00000000000..f8af79e9a09 --- /dev/null +++ b/changelogs/unreleased/211754-automate-dormant-users-deactivation.yml @@ -0,0 +1,5 @@ +--- +title: Automate deactivation of dormant users for self-managed instances +merge_request: 57778 +author: +type: added diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb index 99335321f28..4beaa2da810 100644 --- a/config/initializers/1_settings.rb +++ b/config/initializers/1_settings.rb @@ -569,6 +569,9 @@ Settings.cron_jobs['namespaces_in_product_marketing_emails_worker']['job_class'] Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker'] ||= Settingslogic.new({}) Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker']['cron'] ||= '0 1 * * *' Settings.cron_jobs['ssh_keys_expiring_soon_notification_worker']['job_class'] = 'SshKeys::ExpiringSoonNotificationWorker' +Settings.cron_jobs['users_deactivate_dormant_users_worker'] ||= Settingslogic.new({}) +Settings.cron_jobs['users_deactivate_dormant_users_worker']['cron'] ||= '21,42 0-4 * * *' +Settings.cron_jobs['users_deactivate_dormant_users_worker']['job_class'] = 'Users::DeactivateDormantUsersWorker' Gitlab.com do Settings.cron_jobs['batched_background_migrations_worker'] ||= Settingslogic.new({}) diff --git a/db/migrate/20210421021510_add_deactivate_dormant_users_to_application_settings.rb b/db/migrate/20210421021510_add_deactivate_dormant_users_to_application_settings.rb new file mode 100644 index 00000000000..74d197cd3b8 --- /dev/null +++ b/db/migrate/20210421021510_add_deactivate_dormant_users_to_application_settings.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDeactivateDormantUsersToApplicationSettings < ActiveRecord::Migration[6.0] + def change + add_column :application_settings, :deactivate_dormant_users, :boolean, default: false, null: false + end +end diff --git a/db/migrate/20210421022010_add_index_for_dormant_users.rb b/db/migrate/20210421022010_add_index_for_dormant_users.rb new file mode 100644 index 00000000000..48eff184ca0 --- /dev/null +++ b/db/migrate/20210421022010_add_index_for_dormant_users.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class AddIndexForDormantUsers < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + INDEX_NAME = 'index_users_on_id_and_last_activity_on_for_non_internal_active' + + disable_ddl_transaction! + + def up + index_condition = "state = 'active' AND (users.user_type IS NULL OR users.user_type IN (NULL, 6, 4))" + + add_concurrent_index :users, [:id, :last_activity_on], where: index_condition, name: INDEX_NAME + end + + def down + remove_concurrent_index_by_name :users, INDEX_NAME + end +end diff --git a/db/schema_migrations/20210421021510 b/db/schema_migrations/20210421021510 new file mode 100644 index 00000000000..775f083ac63 --- /dev/null +++ b/db/schema_migrations/20210421021510 @@ -0,0 +1 @@ +6a278c90b8c97fc2255528605ee6bf4547e37ac8c4c17979483ed9db562fa021
\ No newline at end of file diff --git a/db/schema_migrations/20210421022010 b/db/schema_migrations/20210421022010 new file mode 100644 index 00000000000..75abced628d --- /dev/null +++ b/db/schema_migrations/20210421022010 @@ -0,0 +1 @@ +454992d01fa140896ff2a9cea66fb855c9e659a5a7969ac9a3cb5a608de36161
\ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 3aaeb9fab16..e98c724e277 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -9479,6 +9479,7 @@ CREATE TABLE application_settings ( throttle_authenticated_packages_api_period_in_seconds integer DEFAULT 15 NOT NULL, throttle_unauthenticated_packages_api_enabled boolean DEFAULT false NOT NULL, throttle_authenticated_packages_api_enabled boolean DEFAULT false NOT NULL, + deactivate_dormant_users boolean DEFAULT false NOT NULL, CONSTRAINT app_settings_container_reg_cleanup_tags_max_list_size_positive CHECK ((container_registry_cleanup_tags_service_max_list_size >= 0)), CONSTRAINT app_settings_ext_pipeline_validation_service_url_text_limit CHECK ((char_length(external_pipeline_validation_service_url) <= 255)), CONSTRAINT app_settings_registry_exp_policies_worker_capacity_positive CHECK ((container_registry_expiration_policies_worker_capacity >= 0)), @@ -24225,6 +24226,8 @@ CREATE INDEX index_users_on_feed_token ON users USING btree (feed_token); CREATE INDEX index_users_on_group_view ON users USING btree (group_view); +CREATE INDEX index_users_on_id_and_last_activity_on_for_non_internal_active ON users USING btree (id, last_activity_on) WHERE (((state)::text = 'active'::text) AND ((user_type IS NULL) OR (user_type = ANY (ARRAY[NULL::integer, 6, 4])))); + CREATE INDEX index_users_on_incoming_email_token ON users USING btree (incoming_email_token); CREATE INDEX index_users_on_managing_group_id ON users USING btree (managing_group_id); diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index 765478c73cd..fcf9e4b91cf 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -5239,6 +5239,29 @@ The edge type for [`Label`](#label). | <a id="labeledgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | | <a id="labeledgenode"></a>`node` | [`Label`](#label) | The item at the end of the edge. | +#### `LfsObjectRegistryConnection` + +The connection type for [`LfsObjectRegistry`](#lfsobjectregistry). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="lfsobjectregistryconnectionedges"></a>`edges` | [`[LfsObjectRegistryEdge]`](#lfsobjectregistryedge) | A list of edges. | +| <a id="lfsobjectregistryconnectionnodes"></a>`nodes` | [`[LfsObjectRegistry]`](#lfsobjectregistry) | A list of nodes. | +| <a id="lfsobjectregistryconnectionpageinfo"></a>`pageInfo` | [`PageInfo!`](#pageinfo) | Information to aid in pagination. | + +#### `LfsObjectRegistryEdge` + +The edge type for [`LfsObjectRegistry`](#lfsobjectregistry). + +##### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="lfsobjectregistryedgecursor"></a>`cursor` | [`String!`](#string) | A cursor for use in pagination. | +| <a id="lfsobjectregistryedgenode"></a>`node` | [`LfsObjectRegistry`](#lfsobjectregistry) | The item at the end of the edge. | + #### `LicenseHistoryEntryConnection` The connection type for [`LicenseHistoryEntry`](#licensehistoryentry). @@ -8317,6 +8340,22 @@ four standard [pagination arguments](#connection-pagination-arguments): | ---- | ---- | ----------- | | <a id="geonodegroupwikirepositoryregistriesids"></a>`ids` | [`[ID!]`](#id) | Filters registries by their ID. | +##### `GeoNode.lfsObjectRegistries` + +Find LFS object registries on this Geo node. Available only when feature flag `geo_lfs_object_replication` is enabled. + +Returns [`LfsObjectRegistryConnection`](#lfsobjectregistryconnection). + +This field returns a [connection](#connections). It accepts the +four standard [pagination arguments](#connection-pagination-arguments): +`before: String`, `after: String`, `first: Int`, `last: Int`. + +###### Arguments + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="geonodelfsobjectregistriesids"></a>`ids` | [`[ID!]`](#id) | Filters registries by their ID. | + ##### `GeoNode.mergeRequestDiffRegistries` Find merge request diff registries on this Geo node. @@ -9327,6 +9366,23 @@ four standard [pagination arguments](#connection-pagination-arguments): | <a id="labeltitle"></a>`title` | [`String!`](#string) | Content of the label. | | <a id="labelupdatedat"></a>`updatedAt` | [`Time!`](#time) | When this label was last updated. | +### `LfsObjectRegistry` + +Represents the Geo sync and verification state of an LFS object. + +#### Fields + +| Name | Type | Description | +| ---- | ---- | ----------- | +| <a id="lfsobjectregistrycreatedat"></a>`createdAt` | [`Time`](#time) | Timestamp when the LfsObjectRegistry was created. | +| <a id="lfsobjectregistryid"></a>`id` | [`ID!`](#id) | ID of the LfsObjectRegistry. | +| <a id="lfsobjectregistrylastsyncfailure"></a>`lastSyncFailure` | [`String`](#string) | Error message during sync of the LfsObjectRegistry. | +| <a id="lfsobjectregistrylastsyncedat"></a>`lastSyncedAt` | [`Time`](#time) | Timestamp of the most recent successful sync of the LfsObjectRegistry. | +| <a id="lfsobjectregistrylfsobjectid"></a>`lfsObjectId` | [`ID!`](#id) | ID of the LFS object. | +| <a id="lfsobjectregistryretryat"></a>`retryAt` | [`Time`](#time) | Timestamp after which the LfsObjectRegistry should be resynced. | +| <a id="lfsobjectregistryretrycount"></a>`retryCount` | [`Int`](#int) | Number of consecutive failed sync attempts of the LfsObjectRegistry. | +| <a id="lfsobjectregistrystate"></a>`state` | [`RegistryState`](#registrystate) | Sync state of the LfsObjectRegistry. | + ### `LicenseHistoryEntry` Represents an entry from the Cloud License history. diff --git a/doc/development/documentation/styleguide/index.md b/doc/development/documentation/styleguide/index.md index 25cdbaf75ba..912a046c2c0 100644 --- a/doc/development/documentation/styleguide/index.md +++ b/doc/development/documentation/styleguide/index.md @@ -1112,13 +1112,9 @@ document to ensure it links to the most recent version of the file. When documenting navigation through the user interface: - Use the exact wording as shown in the UI, including any capital letters as-is. -- Use bold text for navigation items and the char "greater than" (`>`) as a - separator. For example: `From your project, go to **Settings > CI/CD**`. -- If there are any expandable menus, make sure to mention that the user needs to - expand the tab to find the settings you're referring to. For example: - `From your group, go to **Settings > CI/CD** and expand **General pipelines**`. +- Use bold text for navigation items. -### Navigational elements +### What to call the menus Use these terms when referring to the main GitLab user interface elements: @@ -1130,6 +1126,19 @@ elements: - **Right sidebar**: This is the navigation sidebar on the right of the user interface, specific to the open issue, merge request, or epic. +### How to document the left sidebar + +To be consistent, use this format when you refer to the left sidebar. + +- Go to your project and select **Settings > CI/CD**. +- Go to your group and select **Settings > CI/CD**. +- Go to the Admin Area (**{admin}**) and select **Overview > Projects**. + +For expandable menus, use this format: + +1. Go to your group and select **Settings > CI/CD**. +1. Expand **General pipelines**. + ## Images Images, including screenshots, can help a reader better understand a concept. diff --git a/doc/user/packages/npm_registry/index.md b/doc/user/packages/npm_registry/index.md index b6312002184..8e3718f3255 100644 --- a/doc/user/packages/npm_registry/index.md +++ b/doc/user/packages/npm_registry/index.md @@ -43,7 +43,7 @@ The npm version is shown in the output: ### Install Yarn As an alternative to npm, you can install Yarn in your local environment by following the -instructions at [yarnpkg.com](https://classic.yarnpkg.com/en/docs/install). +instructions at [classic.yarnpkg.com](https://classic.yarnpkg.com/en/docs/install). When installation is complete, verify you can use Yarn in your terminal by running: @@ -305,6 +305,46 @@ See the [Publish npm packages to the GitLab Package Registry using semantic-release](../../../ci/examples/semantic-release.md) step-by-step guide and demo project for a complete example. +## Configure the GitLab npm registry with Yarn 2 + +You can get started with Yarn 2 by following the documentation at +[https://yarnpkg.com/getting-started/install](https://yarnpkg.com/getting-started/install). + +To publish and install with the project-level npm endpoint, set the following configuration in +`.yarnrc.yml`: + +```yaml +npmScopes: + foo: + npmRegistryServer: "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/" + npmPublishRegistry: "https://gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/" + +npmRegistries: + //gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/: + npmAlwaysAuth: true + npmAuthToken: "<your_token>" +``` + +For the instance-level npm endpoint, use this Yarn 2 configuration in `.yarnrc.yml`: + +```yaml +npmScopes: + foo: + npmRegistryServer: "https://gitlab.example.com/api/v4/packages/npm/" + +npmRegistries: + //gitlab.example.com/api/v4/packages/npm/: + npmAlwaysAuth: true + npmAuthToken: "<your_token>" +``` + +In this configuration: + +- Replace `<your_token>` with your personal access token or deploy token. +- Replace `<your_project_id>` with your project's ID, which you can find on the project's home page. +- Replace `gitlab.example.com` with your domain name. +- Your scope is `foo`, without `@`. + ## Publishing packages with the same name or version You cannot publish a package if a package of the same name and version already exists. diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb index 2becbbb9117..b7d667e2556 100644 --- a/lib/gitlab/database/migration_helpers.rb +++ b/lib/gitlab/database/migration_helpers.rb @@ -905,6 +905,10 @@ module Gitlab end end + def convert_to_bigint_column(column) + "#{column}_convert_to_bigint" + end + # Initializes the conversion of a set of integer columns to bigint # # It can be used for converting both a Primary Key and any Foreign Keys @@ -948,7 +952,7 @@ module Gitlab check_trigger_permissions!(table) - conversions = columns.to_h { |column| [column, "#{column}_convert_to_bigint"] } + conversions = columns.to_h { |column| [column, convert_to_bigint_column(column)] } with_lock_retries do conversions.each do |(source_column, temporary_name)| @@ -969,6 +973,20 @@ module Gitlab end end + # Reverts `initialize_conversion_of_integer_to_bigint` + # + # table - The name of the database table containing the columns + # columns - The name, or array of names, of the column(s) that we're converting to bigint. + def revert_initialize_conversion_of_integer_to_bigint(table, columns) + columns = Array.wrap(columns) + temporary_columns = columns.map { |column| convert_to_bigint_column(column) } + + trigger_name = rename_trigger_name(table, columns, temporary_columns) + remove_rename_triggers_for_postgresql(table, trigger_name) + + temporary_columns.each { |column| remove_column(table, column) } + end + # Backfills the new columns used in an integer-to-bigint conversion using background migrations. # # - This helper should be called from a post-deployment migration. @@ -1025,7 +1043,7 @@ module Gitlab conversions = Array.wrap(columns).to_h do |column| raise ArgumentError, "Column #{column} does not exist on #{table}" unless column_exists?(table, column) - temporary_name = "#{column}_convert_to_bigint" + temporary_name = convert_to_bigint_column(column) raise ArgumentError, "Column #{temporary_name} does not exist on #{table}" unless column_exists?(table, temporary_name) [column, temporary_name] @@ -1042,6 +1060,25 @@ module Gitlab sub_batch_size: sub_batch_size) end + # Reverts `backfill_conversion_of_integer_to_bigint` + # + # table - The name of the database table containing the column + # columns - The name, or an array of names, of the column(s) we want to convert to bigint. + # primary_key - The name of the primary key column (most often :id) + def revert_backfill_conversion_of_integer_to_bigint(table, columns, primary_key: :id) + columns = Array.wrap(columns) + + conditions = ActiveRecord::Base.sanitize_sql([ + 'job_class_name = :job_class_name AND table_name = :table_name AND column_name = :column_name AND job_arguments = :job_arguments', + job_class_name: 'CopyColumnUsingBackgroundMigrationJob', + table_name: table, + column_name: primary_key, + job_arguments: [columns, columns.map { |column| convert_to_bigint_column(column) }].to_json + ]) + + execute("DELETE FROM batched_background_migrations WHERE #{conditions}") + end + # Performs a concurrent column rename when using PostgreSQL. def install_rename_triggers_for_postgresql(table, old, new, trigger_name: nil) Gitlab::Database::UnidirectionalCopyTrigger.on_table(table).create(old, new, trigger_name: trigger_name) diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 98e5cd0d9ac..e26ee2ae424 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -14434,6 +14434,9 @@ msgstr "" msgid "Geo|Connection timeout should be between 1-120" msgstr "" +msgid "Geo|Consult Geo troubleshooting information" +msgstr "" + msgid "Geo|Could not remove tracking entry for an existing project." msgstr "" @@ -14506,6 +14509,9 @@ msgstr "" msgid "Geo|Learn more about Geo" msgstr "" +msgid "Geo|Learn more about Geo node statuses" +msgstr "" + msgid "Geo|Make everyone on your team more productive regardless of their location. GitLab Geo creates read-only mirrors of your GitLab instance so you can reduce the time it takes to clone and fetch large repos." msgstr "" @@ -14521,6 +14527,9 @@ msgstr "" msgid "Geo|Node name should be between 1 and 255 characters" msgstr "" +msgid "Geo|Node's status was updated %{timeAgo}." +msgstr "" + msgid "Geo|Not synced yet" msgstr "" @@ -14659,6 +14668,9 @@ msgstr "" msgid "Geo|There are no %{replicable_type} to show" msgstr "" +msgid "Geo|There was an error fetching the Geo Nodes" +msgstr "" + msgid "Geo|Tracking database entry will be removed. Are you sure?" msgstr "" @@ -14680,6 +14692,9 @@ msgstr "" msgid "Geo|Unknown state" msgstr "" +msgid "Geo|Updated %{timeAgo}" +msgstr "" + msgid "Geo|Verification" msgstr "" @@ -32085,9 +32100,6 @@ msgstr "" msgid "There was an error fetching the %{replicableType}" msgstr "" -msgid "There was an error fetching the Geo Nodes" -msgstr "" - msgid "There was an error fetching the Geo Settings" msgstr "" @@ -505,6 +505,7 @@ module QA autoload :WikiPageForm, 'qa/page/component/wiki_page_form' autoload :AccessTokens, 'qa/page/component/access_tokens' autoload :CommitModal, 'qa/page/component/commit_modal' + autoload :VisibilitySetting, 'qa/page/component/visibility_setting' module Issuable autoload :Common, 'qa/page/component/issuable/common' diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 289094268b6..bed6e639cd0 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -132,16 +132,16 @@ module QA all(element_selector_css(name), **kwargs) end - def check_element(name, click_by_js = false) - if find_element(name, visible: false).checked? + def check_element(name, click_by_js = false, visibility = false) + if find_element(name, visible: visibility).checked? QA::Runtime::Logger.debug("#{name} is already checked") return end retry_until(sleep_interval: 1) do - click_checkbox_or_radio(name, click_by_js) - checked = find_element(name, visible: false).checked? + click_checkbox_or_radio(name, click_by_js, visibility) + checked = find_element(name, visible: visibility).checked? QA::Runtime::Logger.debug(checked ? "#{name} was checked" : "#{name} was not checked") @@ -149,16 +149,16 @@ module QA end end - def uncheck_element(name, click_by_js = false) - unless find_element(name, visible: false).checked? + def uncheck_element(name, click_by_js = false, visibility = false) + unless find_element(name, visible: visibility).checked? QA::Runtime::Logger.debug("#{name} is already unchecked") return end retry_until(sleep_interval: 1) do - click_checkbox_or_radio(name, click_by_js) - unchecked = !find_element(name, visible: false).checked? + click_checkbox_or_radio(name, click_by_js, visibility) + unchecked = !find_element(name, visible: visibility).checked? QA::Runtime::Logger.debug(unchecked ? "#{name} was unchecked" : "#{name} was not unchecked") @@ -167,21 +167,22 @@ module QA end # Method for selecting radios - def choose_element(name, click_by_js = false) - if find_element(name, visible: false).checked? + def choose_element(name, click_by_js = false, visibility = false) + if find_element(name, visible: visibility).checked? QA::Runtime::Logger.debug("#{name} is already selected") return end retry_until(sleep_interval: 1) do - click_checkbox_or_radio(name, click_by_js) - selected = find_element(name, visible: false).checked? + click_checkbox_or_radio(name, click_by_js, visibility) + selected = find_element(name, visible: visibility).checked? QA::Runtime::Logger.debug(selected ? "#{name} was selected" : "#{name} was not selected") selected end + wait_for_requests end # Use this to simulate moving the pointer to an element's coordinate @@ -424,8 +425,8 @@ module QA private - def click_checkbox_or_radio(name, click_by_js) - box = find_element(name, visible: false) + def click_checkbox_or_radio(name, click_by_js, visibility) + box = find_element(name, visible: visibility) # Some checkboxes and radio buttons are hidden by their labels and cannot be clicked directly click_by_js ? page.execute_script("arguments[0].click();", box) : box.click end diff --git a/qa/qa/page/component/access_tokens.rb b/qa/qa/page/component/access_tokens.rb index d8e3d12b38b..3c8a6cf6a1d 100644 --- a/qa/qa/page/component/access_tokens.rb +++ b/qa/qa/page/component/access_tokens.rb @@ -19,7 +19,7 @@ module QA end base.view 'app/views/shared/tokens/_scopes_form.html.haml' do - element :api_radio, 'qa-#{scope}-radio' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck + element :api_checkbox, '#{scope}_checkbox' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck end base.view 'app/views/shared/access_tokens/_created_container.html.haml' do @@ -36,7 +36,7 @@ module QA end def check_api - check_element(:api_radio) + check_element(:api_checkbox) end def click_create_token_button diff --git a/qa/qa/page/component/visibility_setting.rb b/qa/qa/page/component/visibility_setting.rb new file mode 100644 index 00000000000..4370cfb4564 --- /dev/null +++ b/qa/qa/page/component/visibility_setting.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module VisibilitySetting + extend QA::Page::PageConcern + + def self.included(base) + super + + base.view 'app/views/shared/_visibility_radios.html.haml' do + element :visibility_radio, 'qa_selector: "#{visibility_level_label(level).downcase}_radio"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck + end + end + + def set_visibility(visibility) + choose_element("#{visibility.downcase}_radio", false, true) + end + end + end + end +end diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index 5f43cfb49c0..9a4e53d6053 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -4,23 +4,20 @@ module QA module Page module Group class New < Page::Base + include Page::Component::VisibilitySetting + view 'app/views/shared/_group_form.html.haml' do - element :group_path_field, 'text_field :path' # rubocop:disable QA/ElementWithPattern - element :group_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern + element :group_path_field + element :group_name_field end view 'app/views/groups/_new_group_fields.html.haml' do element :create_group_button, "submit _('Create group')" # rubocop:disable QA/ElementWithPattern - element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern end def set_path(path) - fill_in 'group_path', with: path - fill_in 'group_name', with: path - end - - def set_visibility(visibility) - choose visibility + fill_element(:group_path_field, path) + fill_element(:group_name_field, path) end def create diff --git a/qa/qa/page/group/settings/general.rb b/qa/qa/page/group/settings/general.rb index 1ab849d10b1..81ab6a0aa84 100644 --- a/qa/qa/page/group/settings/general.rb +++ b/qa/qa/page/group/settings/general.rb @@ -6,6 +6,7 @@ module QA module Settings class General < QA::Page::Base include ::QA::Page::Settings::Common + include Page::Component::VisibilitySetting view 'app/views/groups/edit.html.haml' do element :permission_lfs_2fa_content @@ -21,10 +22,6 @@ module QA element :save_name_visibility_settings_button end - view 'app/views/shared/_visibility_radios.html.haml' do - element :internal_radio, 'qa_selector: "#{visibility_level_label(level).downcase}_radio"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck - end - view 'app/views/groups/settings/_lfs.html.haml' do element :lfs_checkbox end @@ -56,10 +53,6 @@ module QA find_element(:group_name_field).set name end - def set_group_visibility(visibility) - find_element("#{visibility.downcase}_radio").click - end - def click_save_name_visibility_settings_button click_element(:save_name_visibility_settings_button) end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index d1033dbfca9..ebef1a1a972 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -4,8 +4,9 @@ module QA module Page module Project class New < Page::Base - include Page::Component::Select2 include Page::Component::Project::Templates + include Page::Component::Select2 + include Page::Component::VisibilitySetting view 'app/views/projects/new.html.haml' do element :project_create_from_template_tab @@ -59,10 +60,6 @@ module QA click_element(:project_create_from_template_tab) end - def set_visibility(visibility) - choose visibility.capitalize - end - def click_github_link click_link 'GitHub' end diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index 0b4a12dbb2e..96ef9ade292 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -12,7 +12,7 @@ module QA end view 'app/views/projects/_merge_request_merge_method_settings.html.haml' do - element :merge_ff_radio_button + element :merge_ff_radio end view 'app/views/projects/_merge_request_merge_checks_settings.html.haml' do @@ -24,7 +24,7 @@ module QA end def enable_ff_only - click_element(:merge_ff_radio_button) + choose_element(:merge_ff_radio) click_save_changes end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index fd68ac0de16..3f04538a311 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -36,7 +36,7 @@ module QA end view 'app/assets/javascripts/ide/components/commit_sidebar/actions.vue' do - element :commit_to_current_branch_radio + element :commit_to_current_branch_radio_container end view 'app/assets/javascripts/ide/components/commit_sidebar/form.vue' do @@ -44,6 +44,10 @@ module QA element :commit_button end + view 'app/assets/javascripts/ide/components/commit_sidebar/radio_group.vue' do + element :commit_type_radio + end + view 'app/assets/javascripts/ide/components/repo_editor.vue' do element :editor_container end @@ -216,7 +220,9 @@ module QA # animation is still in process even when the buttons have the # expected visibility. commit_success = retry_until(sleep_interval: 5) do - click_element(:commit_to_current_branch_radio) if has_element?(:commit_to_current_branch_radio) + within_element(:commit_to_current_branch_radio_container) do + choose_element(:commit_type_radio) + end click_element(:commit_button) if has_element?(:commit_button) # If this is the first commit, the commit SHA only appears after reloading diff --git a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb index 68367eb67b8..3e378db04d4 100644 --- a/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb +++ b/spec/lib/gitlab/background_migration/copy_column_using_background_migration_job_spec.rb @@ -8,6 +8,10 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo let(:sub_batch_size) { 1000 } let(:pause_ms) { 0 } + let(:helpers) do + ActiveRecord::Migration.new.extend(Gitlab::Database::MigrationHelpers) + end + before do ActiveRecord::Base.connection.execute(<<~SQL) CREATE TABLE #{table_name} @@ -15,8 +19,8 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo id integer NOT NULL, name character varying, fk integer NOT NULL, - id_convert_to_bigint bigint DEFAULT 0 NOT NULL, - fk_convert_to_bigint bigint DEFAULT 0 NOT NULL, + #{helpers.convert_to_bigint_column(:id)} bigint DEFAULT 0 NOT NULL, + #{helpers.convert_to_bigint_column(:fk)} bigint DEFAULT 0 NOT NULL, name_convert_to_text text DEFAULT 'no name' ); SQL @@ -41,18 +45,20 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo let(:migration_class) { described_class.name } it 'copies all primary keys in range' do - copy_columns.perform(12, 15, table_name, 'id', sub_batch_size, pause_ms, 'id', 'id_convert_to_bigint') + temporary_column = helpers.convert_to_bigint_column(:id) + copy_columns.perform(12, 15, table_name, 'id', sub_batch_size, pause_ms, 'id', temporary_column) - expect(test_table.where('id = id_convert_to_bigint').pluck(:id)).to contain_exactly(12, 15) - expect(test_table.where(id_convert_to_bigint: 0).pluck(:id)).to contain_exactly(11, 19) + expect(test_table.where("id = #{temporary_column}").pluck(:id)).to contain_exactly(12, 15) + expect(test_table.where(temporary_column => 0).pluck(:id)).to contain_exactly(11, 19) expect(test_table.all.count).to eq(4) end it 'copies all foreign keys in range' do - copy_columns.perform(10, 14, table_name, 'id', sub_batch_size, pause_ms, 'fk', 'fk_convert_to_bigint') + temporary_column = helpers.convert_to_bigint_column(:fk) + copy_columns.perform(10, 14, table_name, 'id', sub_batch_size, pause_ms, 'fk', temporary_column) - expect(test_table.where('fk = fk_convert_to_bigint').pluck(:id)).to contain_exactly(11, 12) - expect(test_table.where(fk_convert_to_bigint: 0).pluck(:id)).to contain_exactly(15, 19) + expect(test_table.where("fk = #{temporary_column}").pluck(:id)).to contain_exactly(11, 12) + expect(test_table.where(temporary_column => 0).pluck(:id)).to contain_exactly(15, 19) expect(test_table.all.count).to eq(4) end @@ -68,18 +74,20 @@ RSpec.describe Gitlab::BackgroundMigration::CopyColumnUsingBackgroundMigrationJo it 'copies multiple columns when given' do columns_to_copy_from = %w[id fk] - columns_to_copy_to = %w[id_convert_to_bigint fk_convert_to_bigint] + id_tmp_column = helpers.convert_to_bigint_column('id') + fk_tmp_column = helpers.convert_to_bigint_column('fk') + columns_to_copy_to = [id_tmp_column, fk_tmp_column] subject.perform(10, 15, table_name, 'id', sub_batch_size, pause_ms, columns_to_copy_from, columns_to_copy_to) - expect(test_table.where('id = id_convert_to_bigint AND fk = fk_convert_to_bigint').pluck(:id)).to contain_exactly(11, 12, 15) - expect(test_table.where(id_convert_to_bigint: 0).where(fk_convert_to_bigint: 0).pluck(:id)).to contain_exactly(19) + expect(test_table.where("id = #{id_tmp_column} AND fk = #{fk_tmp_column}").pluck(:id)).to contain_exactly(11, 12, 15) + expect(test_table.where(id_tmp_column => 0).where(fk_tmp_column => 0).pluck(:id)).to contain_exactly(19) expect(test_table.all.count).to eq(4) end it 'raises error when number of source and target columns does not match' do columns_to_copy_from = %w[id fk] - columns_to_copy_to = %w[id_convert_to_bigint] + columns_to_copy_to = [helpers.convert_to_bigint_column(:id)] expect do subject.perform(10, 15, table_name, 'id', sub_batch_size, pause_ms, columns_to_copy_from, columns_to_copy_to) diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb index 9eca8dedea9..172af0ade90 100644 --- a/spec/lib/gitlab/database/migration_helpers_spec.rb +++ b/spec/lib/gitlab/database/migration_helpers_spec.rb @@ -4,6 +4,7 @@ require 'spec_helper' RSpec.describe Gitlab::Database::MigrationHelpers do include Database::TableSchemaHelpers + include Database::TriggerHelpers let(:model) do ActiveRecord::Migration.new.extend(described_class) @@ -1702,10 +1703,16 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#convert_to_bigint_column' do + it 'returns the name of the temporary column used to convert to bigint' do + expect(model.convert_to_bigint_column(:id)).to eq('id_convert_to_bigint') + end + end + describe '#initialize_conversion_of_integer_to_bigint' do let(:table) { :test_table } let(:column) { :id } - let(:tmp_column) { "#{column}_convert_to_bigint" } + let(:tmp_column) { model.convert_to_bigint_column(column) } before do model.create_table table, id: false do |t| @@ -1774,11 +1781,7 @@ RSpec.describe Gitlab::Database::MigrationHelpers do context 'when multiple columns are given' do it 'creates the correct columns and installs the trigger' do columns_to_convert = %i[id non_nullable_column nullable_column] - temporary_columns = %w[ - id_convert_to_bigint - non_nullable_column_convert_to_bigint - nullable_column_convert_to_bigint - ] + temporary_columns = columns_to_convert.map { |column| model.convert_to_bigint_column(column) } expect(model).to receive(:add_column).with(table, temporary_columns[0], :bigint, default: 0, null: false) expect(model).to receive(:add_column).with(table, temporary_columns[1], :bigint, default: 0, null: false) @@ -1791,10 +1794,55 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#revert_initialize_conversion_of_integer_to_bigint' do + let(:table) { :test_table } + + before do + model.create_table table, id: false do |t| + t.integer :id, primary_key: true + t.integer :other_id + end + + model.initialize_conversion_of_integer_to_bigint(table, columns) + end + + context 'when single column is given' do + let(:columns) { :id } + + it 'removes column, trigger, and function' do + temporary_column = model.convert_to_bigint_column(:id) + trigger_name = model.rename_trigger_name(table, :id, temporary_column) + + model.revert_initialize_conversion_of_integer_to_bigint(table, columns) + + expect(model.column_exists?(table, temporary_column)).to eq(false) + expect_trigger_not_to_exist(table, trigger_name) + expect_function_not_to_exist(trigger_name) + end + end + + context 'when multiple columns are given' do + let(:columns) { [:id, :other_id] } + + it 'removes column, trigger, and function' do + temporary_columns = columns.map { |column| model.convert_to_bigint_column(column) } + trigger_name = model.rename_trigger_name(table, columns, temporary_columns) + + model.revert_initialize_conversion_of_integer_to_bigint(table, columns) + + temporary_columns.each do |column| + expect(model.column_exists?(table, column)).to eq(false) + end + expect_trigger_not_to_exist(table, trigger_name) + expect_function_not_to_exist(trigger_name) + end + end + end + describe '#backfill_conversion_of_integer_to_bigint' do let(:table) { :_test_backfill_table } let(:column) { :id } - let(:tmp_column) { "#{column}_convert_to_bigint" } + let(:tmp_column) { model.convert_to_bigint_column(column) } before do model.create_table table, id: false do |t| @@ -1872,14 +1920,14 @@ RSpec.describe Gitlab::Database::MigrationHelpers do interval: 120, batch_size: 2, sub_batch_size: 1, - job_arguments: [[column.to_s], ["#{column}_convert_to_bigint"]] + job_arguments: [[column.to_s], [model.convert_to_bigint_column(column)]] ) end end context 'when multiple columns are being converted' do let(:other_column) { :other_id } - let(:other_tmp_column) { "#{other_column}_convert_to_bigint" } + let(:other_tmp_column) { model.convert_to_bigint_column(other_column) } let(:columns) { [column, other_column] } it 'creates the batched migration tracking record' do @@ -1905,6 +1953,54 @@ RSpec.describe Gitlab::Database::MigrationHelpers do end end + describe '#revert_backfill_conversion_of_integer_to_bigint' do + let(:table) { :_test_backfill_table } + let(:primary_key) { :id } + + before do + model.create_table table, id: false do |t| + t.integer primary_key, primary_key: true + t.text :message, null: false + t.integer :other_id + t.timestamps + end + + model.initialize_conversion_of_integer_to_bigint(table, columns, primary_key: primary_key) + model.backfill_conversion_of_integer_to_bigint(table, columns, primary_key: primary_key) + end + + context 'when a single column is being converted' do + let(:columns) { :id } + + it 'deletes the batched migration tracking record' do + expect do + model.revert_backfill_conversion_of_integer_to_bigint(table, columns) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(-1) + end + end + + context 'when a multiple columns are being converted' do + let(:columns) { [:id, :other_id] } + + it 'deletes the batched migration tracking record' do + expect do + model.revert_backfill_conversion_of_integer_to_bigint(table, columns) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(-1) + end + end + + context 'when primary key column has custom name' do + let(:primary_key) { :other_pk } + let(:columns) { :other_id } + + it 'deletes the batched migration tracking record' do + expect do + model.revert_backfill_conversion_of_integer_to_bigint(table, columns, primary_key: primary_key) + end.to change { Gitlab::Database::BackgroundMigration::BatchedMigration.count }.by(-1) + end + end + end + describe '#index_exists_by_name?' do it 'returns true if an index exists' do ActiveRecord::Base.connection.execute( diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 1f7d5c5f91f..3a11bed4efe 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -5620,4 +5620,47 @@ RSpec.describe User do end end end + + describe '.dormant' do + it 'returns dormant users' do + freeze_time do + not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date + too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date + + create(:user, :deactivated, last_activity_on: too_long_ago) + + User::INTERNAL_USER_TYPES.map do |user_type| + create(:user, state: :active, user_type: user_type, last_activity_on: too_long_ago) + end + + create(:user, last_activity_on: not_that_long_ago) + + dormant_user = create(:user, last_activity_on: too_long_ago) + + expect(described_class.dormant).to contain_exactly(dormant_user) + end + end + end + + describe '.with_no_activity' do + it 'returns users with no activity' do + freeze_time do + not_that_long_ago = (described_class::MINIMUM_INACTIVE_DAYS - 1).days.ago.to_date + too_long_ago = described_class::MINIMUM_INACTIVE_DAYS.days.ago.to_date + + create(:user, :deactivated, last_activity_on: nil) + + User::INTERNAL_USER_TYPES.map do |user_type| + create(:user, state: :active, user_type: user_type, last_activity_on: nil) + end + + create(:user, last_activity_on: not_that_long_ago) + create(:user, last_activity_on: too_long_ago) + + user_with_no_activity = create(:user, last_activity_on: nil) + + expect(described_class.with_no_activity).to contain_exactly(user_with_no_activity) + end + end + end end diff --git a/spec/workers/users/deactivate_dormant_users_worker_spec.rb b/spec/workers/users/deactivate_dormant_users_worker_spec.rb new file mode 100644 index 00000000000..32291a143ee --- /dev/null +++ b/spec/workers/users/deactivate_dormant_users_worker_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Users::DeactivateDormantUsersWorker do + describe '#perform' do + subject(:worker) { described_class.new } + + it 'does not run for GitLab.com' do + create(:user, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date) + create(:user, last_activity_on: nil) + + expect(Gitlab).to receive(:com?).and_return(true) + expect(Gitlab::CurrentSettings).not_to receive(:current_application_settings) + + worker.perform + + expect(User.dormant.count).to eq(1) + expect(User.with_no_activity.count).to eq(1) + end + + context 'when automatic deactivation of dormant users is enabled' do + before do + stub_application_setting(deactivate_dormant_users: true) + end + + it 'deactivates dormant users' do + freeze_time do + stub_const("#{described_class.name}::BATCH_SIZE", 1) + stub_const("#{described_class.name}::PAUSE_SECONDS", 0) + + create(:user, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date) + create(:user, last_activity_on: nil) + + expect(worker).to receive(:sleep).twice + + worker.perform + + expect(User.dormant.count).to eq(0) + expect(User.with_no_activity.count).to eq(0) + end + end + end + + context 'when automatic deactivation of dormant users is disabled' do + before do + stub_application_setting(deactivate_dormant_users: false) + end + + it 'does nothing' do + create(:user, last_activity_on: User::MINIMUM_INACTIVE_DAYS.days.ago.to_date) + create(:user, last_activity_on: nil) + + worker.perform + + expect(User.dormant.count).to eq(1) + expect(User.with_no_activity.count).to eq(1) + end + end + end +end |