diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-30 00:09:01 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-11-30 00:09:01 +0000 |
commit | 2fdee6d838d5615a24bfde9874a5c2d84a30d5bf (patch) | |
tree | 30ed88988118d43562d83ff493c7bee31b0e130c | |
parent | 8308674afc1f8636bcd2017e1573292d1500af9d (diff) | |
download | gitlab-ce-2fdee6d838d5615a24bfde9874a5c2d84a30d5bf.tar.gz |
Add latest changes from gitlab-org/gitlab@master
41 files changed, 668 insertions, 68 deletions
diff --git a/.gitlab/ci/global.gitlab-ci.yml b/.gitlab/ci/global.gitlab-ci.yml index 203f4826ae4..8d5a2bef9d6 100644 --- a/.gitlab/ci/global.gitlab-ci.yml +++ b/.gitlab/ci/global.gitlab-ci.yml @@ -293,7 +293,7 @@ - name: postgres:12 command: ["postgres", "-c", "fsync=off", "-c", "synchronous_commit=off", "-c", "full_page_writes=off"] - name: redis:6.0-alpine - - name: elasticsearch:8.4.1 + - name: elasticsearch:8.5.2 variables: POSTGRES_HOST_AUTH_METHOD: trust PG_VERSION: "12" diff --git a/.rubocop_todo/rspec/empty_line_after_example_group.yml b/.rubocop_todo/rspec/empty_line_after_example_group.yml deleted file mode 100644 index e298b1b89b8..00000000000 --- a/.rubocop_todo/rspec/empty_line_after_example_group.yml +++ /dev/null @@ -1,35 +0,0 @@ ---- -# Cop supports --autocorrect. -RSpec/EmptyLineAfterExampleGroup: - Exclude: - - 'ee/spec/controllers/groups/clusters_controller_spec.rb' - - 'ee/spec/features/projects/feature_flags/user_sees_feature_flag_list_spec.rb' - - 'ee/spec/features/security/group/private_access_spec.rb' - - 'ee/spec/lib/gitlab/vulnerabilities/container_scanning_vulnerability_spec.rb' - - 'ee/spec/services/ee/gpg_keys/create_service_spec.rb' - - 'ee/spec/services/ee/issues/create_from_vulnerability_data_service_spec.rb' - - 'ee/spec/services/vulnerabilities/confirm_service_spec.rb' - - 'ee/spec/services/vulnerabilities/dismiss_service_spec.rb' - - 'ee/spec/services/vulnerabilities/resolve_service_spec.rb' - - 'ee/spec/services/vulnerabilities/revert_to_detected_service_spec.rb' - - 'ee/spec/services/vulnerability_issue_links/create_service_spec.rb' - - 'ee/spec/services/vulnerability_issue_links/delete_service_spec.rb' - - 'spec/controllers/explore/projects_controller_spec.rb' - - 'spec/controllers/projects/notes_controller_spec.rb' - - 'spec/factories/projects/ci_feature_usages.rb' - - 'spec/features/security/group/internal_access_spec.rb' - - 'spec/features/security/group/private_access_spec.rb' - - 'spec/features/security/group/public_access_spec.rb' - - 'spec/helpers/blob_helper_spec.rb' - - 'spec/helpers/git_helper_spec.rb' - - 'spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb' - - 'spec/lib/gitlab/blob_helper_spec.rb' - - 'spec/lib/gitlab/file_type_detection_spec.rb' - - 'spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb' - - 'spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb' - - 'spec/models/note_spec.rb' - - 'spec/models/project_feature_spec.rb' - - 'spec/models/user_spec.rb' - - 'spec/models/zoom_meeting_spec.rb' - - 'spec/requests/api/projects_spec.rb' - - 'spec/routing/project_routing_spec.rb' diff --git a/app/controllers/concerns/preferred_language_switcher.rb b/app/controllers/concerns/preferred_language_switcher.rb index 9711e57cf7a..00cd0f9d1d5 100644 --- a/app/controllers/concerns/preferred_language_switcher.rb +++ b/app/controllers/concerns/preferred_language_switcher.rb @@ -16,3 +16,5 @@ module PreferredLanguageSwitcher Gitlab::CurrentSettings.default_preferred_language end end + +PreferredLanguageSwitcher.prepend_mod diff --git a/app/models/project_export_job.rb b/app/models/project_export_job.rb index decc71ee193..47be692d57a 100644 --- a/app/models/project_export_job.rb +++ b/app/models/project_export_job.rb @@ -6,6 +6,13 @@ class ProjectExportJob < ApplicationRecord validates :project, :jid, :status, presence: true + STATUS = { + queued: 0, + started: 1, + finished: 2, + failed: 3 + }.freeze + state_machine :status, initial: :queued do event :start do transition [:queued] => :started @@ -19,9 +26,9 @@ class ProjectExportJob < ApplicationRecord transition [:queued, :started] => :failed end - state :queued, value: 0 - state :started, value: 1 - state :finished, value: 2 - state :failed, value: 3 + state :queued, value: STATUS[:queued] + state :started, value: STATUS[:started] + state :finished, value: STATUS[:finished] + state :failed, value: STATUS[:failed] end end diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb index 43a2f4e9a71..9c3cc803587 100644 --- a/app/services/ci/create_pipeline_service.rb +++ b/app/services/ci/create_pipeline_service.rb @@ -4,8 +4,6 @@ module Ci class CreatePipelineService < BaseService attr_reader :pipeline, :logger - CreateError = Class.new(StandardError) - LOG_MAX_DURATION_THRESHOLD = 3.seconds LOG_MAX_PIPELINE_SIZE = 2_000 LOG_MAX_CREATION_THRESHOLD = 20.seconds diff --git a/app/services/projects/import_export/parallel_export_service.rb b/app/services/projects/import_export/parallel_export_service.rb new file mode 100644 index 00000000000..7e4c0279b06 --- /dev/null +++ b/app/services/projects/import_export/parallel_export_service.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module Projects + module ImportExport + class ParallelExportService + def initialize(export_job, current_user, after_export_strategy) + @export_job = export_job + @current_user = current_user + @after_export_strategy = after_export_strategy + @shared = project.import_export_shared + @logger = Gitlab::Export::Logger.build + end + + def execute + log_info('Parallel project export started') + + if save_exporters && save_export_archive + log_info('Parallel project export finished successfully') + execute_after_export_action(after_export_strategy) + else + notify_error + end + + ensure + cleanup + end + + private + + attr_reader :export_job, :current_user, :after_export_strategy, :shared, :logger + + delegate :project, to: :export_job + + def execute_after_export_action(after_export_strategy) + return if after_export_strategy.execute(current_user, project) + + notify_error + end + + def exporters + [version_saver, exported_relations_merger] + end + + def save_exporters + exporters.all? do |exporter| + log_info("Parallel project export - #{exporter.class.name} saver started") + + exporter.save + end + end + + def save_export_archive + Gitlab::ImportExport::Saver.save(exportable: project, shared: shared) + end + + def version_saver + @version_saver ||= Gitlab::ImportExport::VersionSaver.new(shared: shared) + end + + def exported_relations_merger + @relation_saver ||= Gitlab::ImportExport::Project::ExportedRelationsMerger.new( + export_job: export_job, + shared: shared) + end + + def cleanup + FileUtils.rm_rf(shared.export_path) if File.exist?(shared.export_path) + FileUtils.rm_rf(shared.archive_path) if File.exist?(shared.archive_path) + end + + def log_info(message) + logger.info( + message: message, + **log_base_data + ) + end + + def notify_error + logger.error( + message: 'Parallel project export error', + export_errors: shared.errors.join(', '), + export_job_id: export_job.id, + **log_base_data + ) + + NotificationService.new.project_not_exported(project, current_user, shared.errors) + end + + def log_base_data + { + project_id: project.id, + project_name: project.name, + project_path: project.full_path + } + end + end + end +end diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml index 6a36f85daa4..fa718a9c907 100644 --- a/app/views/shared/_ref_switcher.html.haml +++ b/app/views/shared/_ref_switcher.html.haml @@ -2,7 +2,7 @@ - ref = local_assigns.fetch(:ref, @ref) - form_path = local_assigns.fetch(:form_path, switch_project_refs_path(@project)) -- dropdown_toggle_text = @id || @project.default_branch +- dropdown_toggle_text = ref || @project.default_branch - field_name = local_assigns.fetch(:field_name, 'ref') = form_tag form_path, method: :get, class: "project-refs-form" do diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml index 1b1b68320bc..8cce80e2771 100644 --- a/app/workers/all_queues.yml +++ b/app/workers/all_queues.yml @@ -3009,6 +3009,15 @@ :weight: 1 :idempotent: false :tags: [] +- :name: projects_import_export_parallel_project_export + :worker_name: Projects::ImportExport::ParallelProjectExportWorker + :feature_category: :importers + :has_external_dependencies: false + :urgency: :low + :resource_boundary: :memory + :weight: 1 + :idempotent: true + :tags: [] - :name: projects_import_export_relation_export :worker_name: Projects::ImportExport::RelationExportWorker :feature_category: :importers diff --git a/app/workers/projects/import_export/parallel_project_export_worker.rb b/app/workers/projects/import_export/parallel_project_export_worker.rb new file mode 100644 index 00000000000..ba4194fd4bc --- /dev/null +++ b/app/workers/projects/import_export/parallel_project_export_worker.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module Projects + module ImportExport + class ParallelProjectExportWorker + include ApplicationWorker + include ExceptionBacktrace + + idempotent! + data_consistency :always + deduplicate :until_executed + feature_category :importers + worker_resource_boundary :memory + urgency :low + loggable_arguments 1, 2 + sidekiq_options retries: 3, dead: false, status_expiration: StuckExportJobsWorker::EXPORT_JOBS_EXPIRATION + + sidekiq_retries_exhausted do |job, exception| + export_job = ProjectExportJob.find(job['args'].first) + + export_job.fail_op! + project = export_job.project + + log_payload = { + message: 'Parallel project export error', + export_error: job['error_message'], + project_export_job_id: export_job.id, + project_name: project.name, + project_id: project.id + } + Gitlab::ExceptionLogFormatter.format!(exception, log_payload) + Gitlab::Export::Logger.error(log_payload) + end + + def perform(project_export_job_id, user_id, after_export_strategy = {}) + export_job = ProjectExportJob.find(project_export_job_id) + + return if export_job.finished? + + export_job.update_attribute(:jid, jid) + current_user = User.find(user_id) + after_export = build!(after_export_strategy) + + export_service = ::Projects::ImportExport::ParallelExportService.new(export_job, current_user, after_export) + export_service.execute + + export_job.finish! + rescue Gitlab::ImportExport::AfterExportStrategyBuilder::StrategyNotFoundError + export_job.fail_op! + end + + private + + def build!(after_export_strategy) + strategy_klass = after_export_strategy&.delete('klass') + + Gitlab::ImportExport::AfterExportStrategyBuilder.build!(strategy_klass, after_export_strategy) + end + end + end +end diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml index 2862d13620a..46305512a7a 100644 --- a/config/sidekiq_queues.yml +++ b/config/sidekiq_queues.yml @@ -405,6 +405,8 @@ - 1 - - projects_git_garbage_collect - 1 +- - projects_import_export_parallel_project_export + - 1 - - projects_import_export_relation_export - 1 - - projects_inactive_projects_deletion_notification diff --git a/doc/api/bulk_imports.md b/doc/api/bulk_imports.md index 1e0096a6bdd..a438bc13818 100644 --- a/doc/api/bulk_imports.md +++ b/doc/api/bulk_imports.md @@ -29,7 +29,7 @@ POST /bulk_imports | `entities[source_full_path]` | String | yes | Source full path of the entity to import. | | `entities[destination_name]` | String | yes | Deprecated: Use :destination_slug instead. Destination slug for the entity. | | `entities[destination_slug]` | String | yes | Destination slug for the entity. | -| `entities[destination_namespace]` | String | no | Destination namespace for the entity. | +| `entities[destination_namespace]` | String | yes | Destination namespace for the entity. | ```shell curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/bulk_imports" \ diff --git a/doc/user/packages/yarn_repository/index.md b/doc/user/packages/yarn_repository/index.md new file mode 100644 index 00000000000..7e2f45019cd --- /dev/null +++ b/doc/user/packages/yarn_repository/index.md @@ -0,0 +1,248 @@ +--- +stage: Package +group: Package Registry +info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/product/ux/technical-writing/#assignments +--- + +# Publish packages with Yarn + +Publish npm packages in your project's Package Registry using Yarn. Then install the +packages whenever you need to use them as a dependency. + +Learn how to build a [yarn](../workflows/build_packages.md#yarn) package. + +You can get started with Yarn 2 by following the [Yarn documentation](https://yarnpkg.com/getting-started/install/). + +## Publish to GitLab Package Registry + +### Authentication to the Package Registry + +You need a token to publish a package. Different tokens are available depending on what you're trying to +achieve. For more information, review the [guidance on tokens](../../../user/packages/package_registry/index.md#authenticate-with-the-registry). + +- If your organization uses two-factor authentication (2FA), you must use a personal access token with the scope set to `api`. +- If you publish a package via CI/CD pipelines, you must use a CI job token. + +Create a token and save it to use later in the process. + +### Naming convention + +Depending on how you install the package, you may need to adhere to the naming convention. + +You can use one of two API endpoints to install packages: + +- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace. +- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group. + +If you plan to install a package through the [project level](#install-from-the-project-level), you do not have to +adhere to the naming convention. + +If you plan to install a package through the [instance level](#install-from-the-instance-level), then you must name +your package with a [scope](https://docs.npmjs.com/misc/scope/). Scoped packages begin with a `@` and have the +`@owner/package-name` format. You can set up the scope for your package in the `.yarnrc.yml` file and by using the +`publishConfig` option in the `package.json`. + +- The value used for the `@scope` is the root of the project that hosts the packages and not the root + of the project with the package's source code. The scope should be lowercase. +- The package name can be anything you want + +| Project URL | Package Registry in | Scope | Full package name | +| ------------------------------------------------------- | ------------------- | --------- | ---------------------- | +| `https://gitlab.com/my-org/engineering-group/analytics` | Analytics | `@my-org` | `@my-org/package-name` | + +### Configuring `.yarnrc.yml` to publish from the project level + +To publish with the project-level npm endpoint, set the following configuration in +`.yarnrc.yml`: + +```yaml +npmScopes: + foo: + npmRegistryServer: 'https://<your_domain>/api/v4/projects/<your_project_id>/packages/npm/' + npmPublishRegistry: 'https://<your_domain>/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>' +``` + +In this configuration: + +- Replace `<your_domain>` with your domain name. +- Replace `<your_project_id>` with your project's ID, which you can find on the project's home page. +- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token. + +### Configuring `.yarnrc.yml` to publish from the instance level + +For the instance-level npm endpoint, use this Yarn 2 configuration in `.yarnrc.yml`: + +```yaml +npmScopes: + <scope>: + npmRegistryServer: 'https://<your_domain>/api/v4/packages/npm/' + +npmRegistries: + //gitlab.example.com/api/v4/packages/npm/: + npmAlwaysAuth: true + npmAuthToken: '<your_token>' +``` + +In this configuration: + +- Replace `<your_domain>` with your domain name. +- Your scope is `<scope>`, without `@`. +- Replace `<your_token>` with a deploy token, group access token, project access token, or personal access token. + +### Publishing a package via the command line + +Publish a package: + +```shell +npm publish +``` + +Your package should now publish to the Package Registry. + +### Publishing via a CI/CD pipeline + +In the GitLab project that houses your `yarnrc.yml`, edit or create a `.gitlab-ci.yml` file. For example: + +```yaml +image: node:latest + +stages: + - deploy + +deploy: + stage: deploy + script: + - npm publish +``` + +Your package should now publish to the Package Registry when the pipeline runs. + +## Install a package + +If multiple packages have the same name and version, the most recently-published package is retrieved when you install a package. + +You can install a package from a GitLab project or instance: + +- **Instance-level**: Use when you have many npm packages in different GitLab groups or in their own namespace. +- **Project-level**: Use when you have a few npm packages, and they are not in the same GitLab group. + +### Install from the instance level + +WARNING: +You must use packages published with the scoped [naming convention](#naming-convention) when you install a package from the instance level. + +1. Authenticate to the Package Registry + + If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private. + + ```shell + npm config set -- //your_domain_name/api/v4/packages/npm/:_authToken=your_token + ``` + + - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. + - Replace `your_token` with a deploy token, group access token, project access token, or personal access token. + +1. Set the registry + + ```shell + npm config set @scope:registry https://your_domain_name.com/api/v4/packages/npm/ + ``` + + - Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from. + - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. + - Replace `your_token` with a deploy token, group access token, project access token, or personal access token. + +1. Install the package + + ```shell + yarn add @scope/my-package + ``` + +### Install from the project level + +1. Authenticate to the Package Registry + + If you install a package from a private project, you must authenticate to the Package Registry. Skip this step if the project is not private. + + ```shell + npm config set -- //your_domain_name/api/v4/projects/your_project_id/packages/npm/:_authToken=your_token + ``` + + - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. + - Replace `your_project_id` is your project ID, found on the project's home page. + - Replace `your_token` with a deploy token, group access token, project access token, or personal access token. + +1. Set the registry + + ```shell + npm config set @scope:registry=https://your_domain_name/api/v4/projects/your_project_id/packages/npm/ + ``` + + - Replace `@scope` with the [root level group](#naming-convention) of the project you're installing to the package from. + - Replace `your_domain_name` with your domain name, for example, `gitlab.com`. + - Replace `your_project_id` is your project ID, found on the project's home page. + +1. Install the package + + ```shell + yarn add @scope/my-package + ``` + +## Helpful hints + +For full helpful hints information, refer to the [npm documentation](../npm_registry/index.md#helpful-hints). + +### Supported CLI commands + +The GitLab npm repository supports the following commands for the npm CLI (`npm`) and yarn CLI +(`yarn`): + +- `npm install`: Install npm packages. +- `npm publish`: Publish an npm package to the registry. +- `npm dist-tag add`: Add a dist-tag to an npm package. +- `npm dist-tag ls`: List dist-tags for a package. +- `npm dist-tag rm`: Delete a dist-tag. +- `npm ci`: Install npm packages directly from your `package-lock.json` file. +- `npm view`: Show package metadata. +- `yarn add`: Install an npm package. +- `yarn update`: Update your dependencies. + +## Troubleshooting + +For full troubleshooting information, refer to the [npm documentation](../npm_registry/index.md#troubleshooting). + +### Error running Yarn with the Package Registry for the npm registry + +If you are using [Yarn](https://classic.yarnpkg.com/en/) with the npm registry, you may get +an error message like: + +```shell +yarn install v1.15.2 +warning package.json: No license field +info No lockfile found. +warning XXX: No license field +[1/4] 🔍 Resolving packages... +[2/4] 🚚 Fetching packages... +error An unexpected error occurred: "https://gitlab.example.com/api/v4/projects/XXX/packages/npm/XXX/XXX/-/XXX/XXX-X.X.X.tgz: Request failed \"404 Not Found\"". +info If you think this is a bug, please open a bug report with the information provided in "/Users/XXX/gitlab-migration/module-util/yarn-error.log". +info Visit https://classic.yarnpkg.com/en/docs/cli/install for documentation about this command +``` + +In this case, try adding this to your `.npmrc` file (and replace `<your_token>` +with your personal access token or deploy token): + +```plaintext +//gitlab.example.com/api/v4/projects/:_authToken=<your_token> +``` + +You can also use `yarn config` instead of `npm config` when setting your auth-token dynamically: + +```shell +yarn config set '//gitlab.example.com/api/v4/projects/<your_project_id>/packages/npm/:_authToken' "<your_token>" +yarn config set '//gitlab.example.com/api/v4/packages/npm/:_authToken' "<your_token>" +``` diff --git a/doc/user/profile/notifications.md b/doc/user/profile/notifications.md index 1deb4842107..d0a420a4bbd 100644 --- a/doc/user/profile/notifications.md +++ b/doc/user/profile/notifications.md @@ -76,7 +76,7 @@ For each project and group you can select one of the following levels: | Participate | Receive notifications for threads you have participated in. | | On mention | Receive notifications when you are [mentioned](../discussions/index.md#mentions) in a comment. | | Disabled | Receive no notifications. | -| Custom | Receive notifications for selected events. | +| Custom | Receive notifications for selected events and threads you have participated in. | ### Global notification settings diff --git a/doc/user/project/deploy_tokens/index.md b/doc/user/project/deploy_tokens/index.md index aab72d4859e..cfe7a956bc3 100644 --- a/doc/user/project/deploy_tokens/index.md +++ b/doc/user/project/deploy_tokens/index.md @@ -214,19 +214,3 @@ Prerequisites: - A deploy token with `read_registry` and `write_registry` scopes. Follow the dependency proxy [authentication instructions](../../packages/dependency_proxy/index.md). - -## Troubleshooting - -### Error: `api error: Repository or object not found:` - -When using a group deploy token to clone from LFS objects, you might get `404 Not Found` responses -and this error message. This occurs because of a bug, documented in -[issue 235398](https://gitlab.com/gitlab-org/gitlab/-/issues/235398). - -```plaintext -api error: Repository or object not found: -https://<URL-with-token>.git/info/lfs/objects/batch -Check that it exists and that you have proper access to it -``` - -The workaround is to use a project deploy token. diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 607cce02400..5c61b83fed7 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -22032,6 +22032,9 @@ msgstr "" msgid "Integrations|Enable comments" msgstr "" +msgid "Integrations|Enable slash commands and notifications for a Slack workspace." +msgstr "" + msgid "Integrations|Ensure your instance URL is correct and your instance is configured correctly. %{linkStart}Learn more%{linkEnd}." msgstr "" diff --git a/spec/commands/sidekiq_cluster/cli_spec.rb b/spec/commands/sidekiq_cluster/cli_spec.rb index 4d1a07a6a75..0b73a62e1e0 100644 --- a/spec/commands/sidekiq_cluster/cli_spec.rb +++ b/spec/commands/sidekiq_cluster/cli_spec.rb @@ -245,9 +245,9 @@ RSpec.describe Gitlab::SidekiqCluster::CLI, stub_settings_source: true do # rubo it 'expands multiple queue groups correctly' do expected_workers = if Gitlab.ee? - [%w[chat_notification], %w[project_export projects_import_export_relation_export project_template_export]] + [%w[chat_notification], %w[project_export projects_import_export_parallel_project_export projects_import_export_relation_export project_template_export]] else - [%w[chat_notification], %w[project_export projects_import_export_relation_export]] + [%w[chat_notification], %w[project_export projects_import_export_parallel_project_export projects_import_export_relation_export]] end expect(Gitlab::SidekiqCluster) diff --git a/spec/controllers/explore/projects_controller_spec.rb b/spec/controllers/explore/projects_controller_spec.rb index 5c977439af4..a79d9fa1276 100644 --- a/spec/controllers/explore/projects_controller_spec.rb +++ b/spec/controllers/explore/projects_controller_spec.rb @@ -101,6 +101,7 @@ RSpec.describe Explore::ProjectsController do expect(response).to have_gitlab_http_status(:not_found) end end + context 'when topic exists' do before do create(:topic, name: 'topic1') diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb index 383e8112315..0afd2e10ea2 100644 --- a/spec/controllers/projects/notes_controller_spec.rb +++ b/spec/controllers/projects/notes_controller_spec.rb @@ -757,6 +757,7 @@ RSpec.describe Projects::NotesController do expect { put :update, params: request_params }.to change { note.reload.note } end end + context "doesnt update the note" do let(:issue) { create(:issue, :confidential, project: project) } let(:note) { create(:note, noteable: issue, project: project) } diff --git a/spec/factories/project_export_jobs.rb b/spec/factories/project_export_jobs.rb index b2666555ea8..bf8cfd863ec 100644 --- a/spec/factories/project_export_jobs.rb +++ b/spec/factories/project_export_jobs.rb @@ -4,5 +4,21 @@ FactoryBot.define do factory :project_export_job do project jid { SecureRandom.hex(8) } + + trait :queued do + status { ProjectExportJob::STATUS[:queued] } + end + + trait :started do + status { ProjectExportJob::STATUS[:started] } + end + + trait :finished do + status { ProjectExportJob::STATUS[:finished] } + end + + trait :failed do + status { ProjectExportJob::STATUS[:failed] } + end end end diff --git a/spec/factories/projects/ci_feature_usages.rb b/spec/factories/projects/ci_feature_usages.rb index 1ab1d82ef4b..48e5331afcc 100644 --- a/spec/factories/projects/ci_feature_usages.rb +++ b/spec/factories/projects/ci_feature_usages.rb @@ -4,6 +4,7 @@ FactoryBot.define do factory :project_ci_feature_usage, class: 'Projects::CiFeatureUsage' do project factory: :project feature { :code_coverage } # rubocop: disable RSpec/EmptyExampleGroup + default_branch { false } end end diff --git a/spec/features/security/group/internal_access_spec.rb b/spec/features/security/group/internal_access_spec.rb index 904431b4a0f..ad2df4a1882 100644 --- a/spec/features/security/group/internal_access_spec.rb +++ b/spec/features/security/group/internal_access_spec.rb @@ -27,9 +27,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -47,9 +49,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -69,9 +73,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -89,9 +95,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -109,9 +117,11 @@ RSpec.describe 'Internal Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_denied_for(:maintainer).of(group) } it { is_expected.to be_denied_for(:developer).of(group) } diff --git a/spec/features/security/group/private_access_spec.rb b/spec/features/security/group/private_access_spec.rb index 3d56468a1c9..2e7b7512b45 100644 --- a/spec/features/security/group/private_access_spec.rb +++ b/spec/features/security/group/private_access_spec.rb @@ -27,9 +27,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -47,9 +49,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -69,9 +73,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -89,9 +95,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -109,9 +117,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_denied_for(:maintainer).of(group) } it { is_expected.to be_denied_for(:developer).of(group) } @@ -135,9 +145,11 @@ RSpec.describe 'Private Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } diff --git a/spec/features/security/group/public_access_spec.rb b/spec/features/security/group/public_access_spec.rb index ac6b8a8ddd1..513c5710c8f 100644 --- a/spec/features/security/group/public_access_spec.rb +++ b/spec/features/security/group/public_access_spec.rb @@ -27,9 +27,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -47,9 +49,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -69,9 +73,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -89,9 +95,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_allowed_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_allowed_for(:maintainer).of(group) } it { is_expected.to be_allowed_for(:developer).of(group) } @@ -109,9 +117,11 @@ RSpec.describe 'Public Group access', feature_category: :permissions do context 'when admin mode is enabled', :enable_admin_mode do it { is_expected.to be_allowed_for(:admin) } end + context 'when admin mode is disabled' do it { is_expected.to be_denied_for(:admin) } end + it { is_expected.to be_allowed_for(:owner).of(group) } it { is_expected.to be_denied_for(:maintainer).of(group) } it { is_expected.to be_denied_for(:developer).of(group) } diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index fe652e905cc..dac0d3fe182 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -80,6 +80,7 @@ RSpec.describe BlobHelper do end end end + context 'viewer related' do include FakeBlobHelpers diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb index 0dd9eecb7f0..543b9ce7a82 100644 --- a/spec/helpers/git_helper_spec.rb +++ b/spec/helpers/git_helper_spec.rb @@ -15,11 +15,13 @@ RSpec.describe GitHelper do it { expect(strip_signature).to eq("Version 1.69.0\n\n") } end + context 'strips PGP MESSAGE' do let(:strip_signature) { helper.strip_signature( pgp_message_tag ) } it { expect(strip_signature).to eq("Version 1.69.0\n\n") } end + context 'strips SIGNED MESSAGE' do let(:strip_signature) { helper.strip_signature( x509_message_tag ) } diff --git a/spec/initializers/lograge_spec.rb b/spec/initializers/lograge_spec.rb index 0a794e8ebcd..5be99be61ae 100644 --- a/spec/initializers/lograge_spec.rb +++ b/spec/initializers/lograge_spec.rb @@ -96,7 +96,7 @@ RSpec.describe 'lograge', type: :request do skip_memory_instrumentation! end - it 'logs memory usage metrics' do + it 'logs memory usage metrics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do expect(Lograge.formatter).to receive(:call) .with(a_hash_including(:mem_objects)) .and_call_original diff --git a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb index 97fcddefd42..19e3a1fecc3 100644 --- a/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb +++ b/spec/lib/bulk_imports/projects/pipelines/issues_pipeline_spec.rb @@ -89,6 +89,7 @@ RSpec.describe BulkImports::Projects::Pipelines::IssuesPipeline do expect(award_emoji.user).to eq(user) end end + context 'issue state' do let(:issue_attributes) { { 'state' => 'closed' } } diff --git a/spec/lib/gitlab/blob_helper_spec.rb b/spec/lib/gitlab/blob_helper_spec.rb index a2f20dcd4fc..e18277ba8d2 100644 --- a/spec/lib/gitlab/blob_helper_spec.rb +++ b/spec/lib/gitlab/blob_helper_spec.rb @@ -68,6 +68,7 @@ RSpec.describe Gitlab::BlobHelper do expect(blob.image?).to be_falsey end end + context 'with a .webp file' do it 'returns true' do expect(webp_blob.image?).to be_truthy diff --git a/spec/lib/gitlab/file_type_detection_spec.rb b/spec/lib/gitlab/file_type_detection_spec.rb index c435d3f6097..1be0f7d53fa 100644 --- a/spec/lib/gitlab/file_type_detection_spec.rb +++ b/spec/lib/gitlab/file_type_detection_spec.rb @@ -31,6 +31,7 @@ RSpec.describe Gitlab::FileTypeDetection do expect(described_class.extension_match?('my/file.foo', extensions)).to eq(true) end end + context 'when class is an uploader' do let(:uploader) do example_uploader = Class.new(CarrierWave::Uploader::Base) do diff --git a/spec/lib/gitlab/instrumentation_helper_spec.rb b/spec/lib/gitlab/instrumentation_helper_spec.rb index 38607ce2752..f3df96fd38a 100644 --- a/spec/lib/gitlab/instrumentation_helper_spec.rb +++ b/spec/lib/gitlab/instrumentation_helper_spec.rb @@ -129,7 +129,7 @@ RSpec.describe Gitlab::InstrumentationHelper do skip_memory_instrumentation! end - it 'logs memory usage metrics' do + it 'logs memory usage metrics', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do subject expect(payload).to include( diff --git a/spec/lib/gitlab/memory/instrumentation_spec.rb b/spec/lib/gitlab/memory/instrumentation_spec.rb index 069c45da18a..6489cda3978 100644 --- a/spec/lib/gitlab/memory/instrumentation_spec.rb +++ b/spec/lib/gitlab/memory/instrumentation_spec.rb @@ -10,7 +10,7 @@ RSpec.describe Gitlab::Memory::Instrumentation do end describe '.available?' do - it 'returns true' do + it 'returns true', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do expect(described_class).to be_available end end @@ -18,7 +18,7 @@ RSpec.describe Gitlab::Memory::Instrumentation do describe '.start_thread_memory_allocations' do subject { described_class.start_thread_memory_allocations } - it 'a hash is returned' do + it 'a hash is returned', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do is_expected.to be_a(Hash) end @@ -47,7 +47,7 @@ RSpec.describe Gitlab::Memory::Instrumentation do expect(described_class).to receive(:measure_thread_memory_allocations).and_call_original end - it 'a hash is returned' do + it 'a hash is returned', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/384081' do result = subject expect(result).to include( mem_objects: be > 1000, diff --git a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb index 8c9a1abba5a..a2b79f7412e 100644 --- a/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb +++ b/spec/lib/gitlab/sidekiq_daemon/memory_killer_spec.rb @@ -452,6 +452,7 @@ RSpec.describe Gitlab::SidekiqDaemon::MemoryKiller do expect(subject).to eq("current_rss(#{rss}) > soft_limit_rss(#{soft_limit}) longer than GRACE_BALLOON_SECONDS(#{grace_balloon_seconds})") end end + context 'deadline not exceeded' do let(:deadline_exceeded) { false } diff --git a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb index e0ebb86585a..2df86804f34 100644 --- a/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb +++ b/spec/models/concerns/token_authenticatable_strategies/encrypted_spec.rb @@ -237,6 +237,7 @@ RSpec.describe TokenAuthenticatableStrategies::Encrypted do expect(subject.set_token(instance, 'my-value')).to eq 'my-value' end end + context 'when encryption is optional' do let(:options) { { encrypted: :optional } } diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb index 7c71080d63e..328d3ba7dda 100644 --- a/spec/models/note_spec.rb +++ b/spec/models/note_spec.rb @@ -579,6 +579,7 @@ RSpec.describe Note do expect(commit_note.confidential?).to be_falsy end end + context 'when note is confidential' do it 'is true even when a noteable is not confidential' do issue = create(:issue, confidential: false) diff --git a/spec/models/project_feature_spec.rb b/spec/models/project_feature_spec.rb index dae0f84eda3..fb6aaffdf22 100644 --- a/spec/models/project_feature_spec.rb +++ b/spec/models/project_feature_spec.rb @@ -291,6 +291,7 @@ RSpec.describe ProjectFeature do end end end + # rubocop:disable Gitlab/FeatureAvailableUsage describe '#feature_available?' do let(:features) { ProjectFeature::FEATURES } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index 021083dc918..95014641abb 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -3217,6 +3217,7 @@ RSpec.describe User do expect(described_class.find_by_full_path('unknown')).to eq(nil) end end + context 'with the follow_redirects option set to true' do it 'returns nil' do expect(described_class.find_by_full_path('unknown', follow_redirects: true)).to eq(nil) diff --git a/spec/models/zoom_meeting_spec.rb b/spec/models/zoom_meeting_spec.rb index 2b45533035d..d3d75a19fed 100644 --- a/spec/models/zoom_meeting_spec.rb +++ b/spec/models/zoom_meeting_spec.rb @@ -29,6 +29,7 @@ RSpec.describe ZoomMeeting do expect(meetings_added).not_to include(removed_meeting.id) end end + describe '.removed_from_issue' do it 'gets only removed meetings' do meetings_removed = described_class.removed_from_issue.pluck(:id) diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb index 3831e8e1dfe..d8a0af4851b 100644 --- a/spec/requests/api/projects_spec.rb +++ b/spec/requests/api/projects_spec.rb @@ -4687,6 +4687,7 @@ RSpec.describe API::Projects do end end end + describe 'PUT /projects/:id/transfer' do context 'when authenticated as owner' do let(:group) { create :group } diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb index 42196a7d8af..4448abe1638 100644 --- a/spec/routing/project_routing_spec.rb +++ b/spec/routing/project_routing_spec.rb @@ -301,6 +301,7 @@ RSpec.describe 'project routing' do expect(get('/gitlab/gitlabhq/-/merge_requests/1/conflicts')).to route_to('projects/merge_requests/conflicts#show', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1') end end + # raw_project_snippet GET /:project_id/snippets/:id/raw(.:format) snippets#raw # project_snippets GET /:project_id/snippets(.:format) snippets#index # new_project_snippet GET /:project_id/snippets/new(.:format) snippets#new diff --git a/spec/services/projects/import_export/parallel_export_service_spec.rb b/spec/services/projects/import_export/parallel_export_service_spec.rb new file mode 100644 index 00000000000..b9f2867077c --- /dev/null +++ b/spec/services/projects/import_export/parallel_export_service_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ImportExport::ParallelExportService, feature_category: :importers do + let_it_be(:user) { create(:user) } + + let(:export_job) { create(:project_export_job) } + let(:after_export_strategy) { Gitlab::ImportExport::AfterExportStrategies::DownloadNotificationStrategy.new } + let(:project) { export_job.project } + + before do + allow_next_instance_of(Gitlab::ImportExport::Project::ExportedRelationsMerger) do |saver| + allow(saver).to receive(:save).and_return(true) + end + + allow_next_instance_of(Gitlab::ImportExport::VersionSaver) do |saver| + allow(saver).to receive(:save).and_return(true) + end + end + + describe '#execute' do + subject(:service) { described_class.new(export_job, user, after_export_strategy) } + + it 'creates a project export archive file' do + expect(Gitlab::ImportExport::Saver).to receive(:save) + .with(exportable: project, shared: project.import_export_shared) + + service.execute + end + + it 'logs export progress' do + allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true) + + logger = service.instance_variable_get(:@logger) + messages = [ + 'Parallel project export started', + 'Parallel project export - Gitlab::ImportExport::VersionSaver saver started', + 'Parallel project export - Gitlab::ImportExport::Project::ExportedRelationsMerger saver started', + 'Parallel project export finished successfully' + ] + messages.each do |message| + expect(logger).to receive(:info).ordered.with(hash_including(message: message)) + end + + service.execute + end + + it 'executes after export stragegy on export success' do + allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true) + + expect(after_export_strategy).to receive(:execute) + + service.execute + end + + it 'ensures files are cleaned up' do + shared = project.import_export_shared + FileUtils.mkdir_p(shared.archive_path) + FileUtils.mkdir_p(shared.export_path) + + allow(Gitlab::ImportExport::Saver).to receive(:save).and_raise(StandardError) + + expect { service.execute }.to raise_error(StandardError) + + expect(File.exist?(shared.export_path)).to eq(false) + expect(File.exist?(shared.archive_path)).to eq(false) + end + + context 'when export fails' do + it 'notifies the error to the user' do + allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(false) + + allow(project.import_export_shared).to receive(:errors).and_return(['Error']) + + expect_next_instance_of(NotificationService) do |instance| + expect(instance).to receive(:project_not_exported).with(project, user, ['Error']) + end + + service.execute + end + end + + context 'when after export stragegy fails' do + it 'notifies the error to the user' do + allow(Gitlab::ImportExport::Saver).to receive(:save).and_return(true) + allow(after_export_strategy).to receive(:execute).and_return(false) + allow(project.import_export_shared).to receive(:errors).and_return(['Error']) + + expect_next_instance_of(NotificationService) do |instance| + expect(instance).to receive(:project_not_exported).with(project, user, ['Error']) + end + + service.execute + end + end + end +end diff --git a/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb b/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb new file mode 100644 index 00000000000..d3ac0a34295 --- /dev/null +++ b/spec/workers/projects/import_export/parallel_project_export_worker_spec.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Projects::ImportExport::ParallelProjectExportWorker, feature_category: :importers do + let_it_be(:user) { create(:user) } + + let(:export_job) { create(:project_export_job, :started) } + let(:after_export_strategy) { {} } + let(:job_args) { [export_job.id, user.id, after_export_strategy] } + + before do + allow_next_instance_of(described_class) do |job| + allow(job).to receive(:jid) { SecureRandom.hex(8) } + end + end + + describe '#perform' do + it_behaves_like 'an idempotent worker' do + it 'sets the export job status to finished' do + subject + + expect(export_job.reload.finished?).to eq(true) + end + end + + context 'when after export strategy does not exist' do + let(:after_export_strategy) { { 'klass' => 'InvalidStrategy' } } + + it 'sets the export job status to failed' do + described_class.new.perform(*job_args) + + expect(export_job.reload.failed?).to eq(true) + end + end + end + + describe '.sidekiq_retries_exhausted' do + let(:job) { { 'args' => job_args, 'error_message' => 'Error message' } } + + it 'sets export_job status to failed' do + described_class.sidekiq_retries_exhausted_block.call(job) + + expect(export_job.reload.failed?).to eq(true) + end + + it 'logs an error message' do + expect_next_instance_of(Gitlab::Export::Logger) do |logger| + expect(logger).to receive(:error).with( + hash_including( + message: 'Parallel project export error', + export_error: 'Error message' + ) + ) + end + + described_class.sidekiq_retries_exhausted_block.call(job) + end + end +end |