+
+
Array | List of analyzers which are available for the project. |
+| `enabled` | SecurityScannerType! => Array | List of analyzers which are enabled for the project. |
+| `pipelineRun` | SecurityScannerType! => Array | List of analyzers which ran successfully in the latest pipeline. |
+
## SentryDetailedError
A Sentry error.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 5ac087a0aa4..43e66adbd35 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -129,7 +129,7 @@ Its feature set is listed on the table below according to DevOps stages.
| [Browser Performance Testing](../user/project/merge_requests/browser_performance_testing.md) | Quickly determine the browser performance impact of pending code changes. |
| [Load Performance Testing](../user/project/merge_requests/load_performance_testing.md) | Quickly determine the server performance impact of pending code changes. |
| [CI services](services/README.md) | Link Docker containers with your base image.|
-| [Code Quality](../user/project/merge_requests/code_quality.md) **(STARTER)** | Analyze your source code quality. |
+| [Code Quality](../user/project/merge_requests/code_quality.md) | Analyze your source code quality. |
| [GitLab CI/CD for external repositories](ci_cd_for_external_repos/index.md) **(PREMIUM)** | Get the benefits of GitLab CI/CD combined with repositories in GitHub and Bitbucket Cloud. |
| [Interactive Web Terminals](interactive_web_terminal/index.md) **(CORE ONLY)** | Open an interactive web terminal to debug the running jobs. |
| [JUnit tests](junit_test_reports.md) | Identify script failures directly on merge requests. |
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index 3931d89e7c2..db18624d4e9 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -188,7 +188,7 @@ according to each stage (Verify, Package, Release).
1. **Verify**:
- Automatically build and test your application with Continuous Integration.
- - Analyze your source code quality with [GitLab Code Quality](../../user/project/merge_requests/code_quality.md). **(STARTER)**
+ - Analyze your source code quality with [GitLab Code Quality](../../user/project/merge_requests/code_quality.md).
- Determine the browser performance impact of code changes with [Browser Performance Testing](../../user/project/merge_requests/browser_performance_testing.md). **(PREMIUM)**
- Determine the server performance impact of code changes with [Load Performance Testing](../../user/project/merge_requests/load_performance_testing.md). **(PREMIUM)**
- Perform a series of tests, such as [Container Scanning](../../user/application_security/container_scanning/index.md) **(ULTIMATE)**, [Dependency Scanning](../../user/application_security/dependency_scanning/index.md) **(ULTIMATE)**, and [JUnit tests](../junit_test_reports.md).
diff --git a/doc/ci/pipelines/job_artifacts.md b/doc/ci/pipelines/job_artifacts.md
index 3279d720bf0..c4457d17dc2 100644
--- a/doc/ci/pipelines/job_artifacts.md
+++ b/doc/ci/pipelines/job_artifacts.md
@@ -146,9 +146,10 @@ plan report will be uploaded to GitLab as an artifact and will be automatically
in merge requests. For more information, see
[Output `terraform plan` information into a merge request](../../user/infrastructure/index.md#output-terraform-plan-information-into-a-merge-request).
-#### `artifacts:reports:codequality` **(STARTER)**
+#### `artifacts:reports:codequality`
-> - Introduced in GitLab 11.5.
+> - Introduced in [GitLab Starter](https://about.gitlab.com/pricing/) 11.5.
+> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) in GitLab 13.2.
> - Requires GitLab Runner 11.5 and above.
The `codequality` report collects [CodeQuality issues](../../user/project/merge_requests/code_quality.md)
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 49ac2f85bc3..6317d5c687c 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -117,7 +117,7 @@ The following table lists available parameters for jobs:
| [`when`](#when) | When to run job. Also available: `when:manual` and `when:delayed`. |
| [`environment`](#environment) | Name of an environment to which the job deploys. Also available: `environment:name`, `environment:url`, `environment:on_stop`, `environment:auto_stop_in` and `environment:action`. |
| [`cache`](#cache) | List of files that should be cached between subsequent runs. Also available: `cache:paths`, `cache:key`, `cache:untracked`, and `cache:policy`. |
-| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.
In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:codequality`, `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0), `artifacts:reports:performance`, `artifacts:reports:load_performance`, and `artifacts:reports:metrics`. |
+| [`artifacts`](#artifacts) | List of files and directories to attach to a job on success. Also available: `artifacts:paths`, `artifacts:exclude`, `artifacts:expose_as`, `artifacts:name`, `artifacts:untracked`, `artifacts:when`, `artifacts:expire_in`, `artifacts:reports`, `artifacts:reports:codequality`, `artifacts:reports:junit`, `artifacts:reports:cobertura`, and `artifacts:reports:terraform`.
In GitLab [Enterprise Edition](https://about.gitlab.com/pricing/), these are available: `artifacts:reports:sast`, `artifacts:reports:dependency_scanning`, `artifacts:reports:container_scanning`, `artifacts:reports:dast`, `artifacts:reports:license_scanning`, `artifacts:reports:license_management` (removed in GitLab 13.0), `artifacts:reports:performance`, `artifacts:reports:load_performance`, and `artifacts:reports:metrics`. |
| [`dependencies`](#dependencies) | Restrict which artifacts are passed to a specific job by providing a list of jobs to fetch artifacts from. |
| [`coverage`](#coverage) | Code coverage settings for a given job. |
| [`retry`](#retry) | When and how many times a job can be auto-retried in case of a failure. |
@@ -3147,7 +3147,7 @@ These are the available report types:
| [`artifacts:reports:dotenv`](../pipelines/job_artifacts.md#artifactsreportsdotenv) | The `dotenv` report collects a set of environment variables. |
| [`artifacts:reports:cobertura`](../pipelines/job_artifacts.md#artifactsreportscobertura) | The `cobertura` report collects Cobertura coverage XML files. |
| [`artifacts:reports:terraform`](../pipelines/job_artifacts.md#artifactsreportsterraform) | The `terraform` report collects Terraform `tfplan.json` files. |
-| [`artifacts:reports:codequality`](../pipelines/job_artifacts.md#artifactsreportscodequality-starter) **(STARTER)** | The `codequality` report collects CodeQuality issues. |
+| [`artifacts:reports:codequality`](../pipelines/job_artifacts.md#artifactsreportscodequality) | The `codequality` report collects CodeQuality issues. |
| [`artifacts:reports:sast`](../pipelines/job_artifacts.md#artifactsreportssast-ultimate) **(ULTIMATE)** | The `sast` report collects Static Application Security Testing vulnerabilities. |
| [`artifacts:reports:dependency_scanning`](../pipelines/job_artifacts.md#artifactsreportsdependency_scanning-ultimate) **(ULTIMATE)** | The `dependency_scanning` report collects Dependency Scanning vulnerabilities. |
| [`artifacts:reports:container_scanning`](../pipelines/job_artifacts.md#artifactsreportscontainer_scanning-ultimate) **(ULTIMATE)** | The `container_scanning` report collects Container Scanning vulnerabilities. |
diff --git a/doc/development/documentation/site_architecture/global_nav.md b/doc/development/documentation/site_architecture/global_nav.md
index ba265e26b52..646626eadf1 100644
--- a/doc/development/documentation/site_architecture/global_nav.md
+++ b/doc/development/documentation/site_architecture/global_nav.md
@@ -16,6 +16,22 @@ Global navigation (the left-most pane in our three pane documentation) provides:
- The ability to refine landing pages, so they don't have to do all the work of surfacing
every page contained within the documentation.
+## Quick start
+
+To add a topic to the global nav, go to the directory that contains
+[navigation files](https://gitlab.com/gitlab-org/gitlab-docs/blob/master/content/_data/)
+and edit the `yaml` file for your product area. You can copy an existing nav entry and
+edit it to point to your topic.
+
+The files are:
+
+| File | Document | Location |
+|-----------------------|--------------------------------------------------------------------|-------------------------------------------------------|
+| `charts-nav.yaml` | GitLab cloud native Helm Chart | `https://docs.gitlab.com/charts/` |
+| `default-nav.yaml` | GitLab Docs | `https://docs.gitlab.com/ee/` |
+| `omnibus-nav.yaml` | Omnibus GitLab Docs | `https://docs.gitlab.com/omnibus/` |
+| `runner-nav.yaml` | GitLab Runner Docs | `https://docs.gitlab.com/runner/` |
+
## Adding new items
All new pages need a new navigation item. Without a navigation, the page becomes "orphaned". That
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index 1219ba9eced..f2c90e71bd5 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -1346,6 +1346,11 @@ Tagged and released versions of GitLab documentation are available:
The version introducing a new feature is added to the top of the topic in the documentation to provide
a helpful link back to how the feature was developed.
+TIP: **Tip:**
+Whenever you have documentation related to the `gitlab.rb` file, you're working with a self-managed installation.
+The section or page is therefore likely to apply only to self-managed instances.
+If so, the relevant "`TIER` ONLY" [Product badge](#product-badges) should be included at the highest applicable heading level.
+
### Text for documentation requiring version text
- For features that need to declare the GitLab version that the feature was introduced. Text similar
diff --git a/doc/development/sidekiq_style_guide.md b/doc/development/sidekiq_style_guide.md
index 2793756ff64..421d873eb71 100644
--- a/doc/development/sidekiq_style_guide.md
+++ b/doc/development/sidekiq_style_guide.md
@@ -64,6 +64,36 @@ the extra jobs will take resources away from jobs from workers that were already
there, if the resources available to the Sidekiq process handling the namespace
are not adjusted appropriately.
+## Versioning
+
+Version can be specified on each Sidekiq worker class.
+This is then sent along when the job is created.
+
+```ruby
+class FooWorker
+ include ApplicationWorker
+
+ version 2
+
+ def perform(*args)
+ if job_version == 2
+ foo = args.first['foo']
+ else
+ foo = args.first
+ end
+ end
+end
+```
+
+Under this schema, any worker is expected to be able to handle any job that was
+enqueued by an older version of that worker. This means that when changing the
+arguments a worker takes, you must increment the `version` (or set `version 1`
+if this is the first time a worker's arguments are changing), but also make sure
+that the worker is still able to handle jobs that were queued with any earlier
+version of the arguments. From the worker's `perform` method, you can read
+`self.job_version` if you want to specifically branch on job version, or you
+can read the number or type of provided arguments.
+
## Idempotent Jobs
It's known that a job can fail for multiple reasons. For example, network outages or bugs.
diff --git a/doc/user/project/merge_requests/code_quality.md b/doc/user/project/merge_requests/code_quality.md
index 1d360728c54..36acba032ff 100644
--- a/doc/user/project/merge_requests/code_quality.md
+++ b/doc/user/project/merge_requests/code_quality.md
@@ -5,7 +5,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
type: reference, howto
---
-# Code Quality **(STARTER)**
+# Code Quality
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1984) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
@@ -25,6 +25,11 @@ Code Quality:
DevOps](../../../topics/autodevops/stages.md#auto-code-quality-starter).
- Can be extended through [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) or a [custom tool](#implementing-a-custom-tool).
+## Code Quality Widget
+
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/1984) in [GitLab Starter](https://about.gitlab.com/pricing/) 9.3.
+> - Made [available in all tiers](https://gitlab.com/gitlab-org/gitlab/-/issues/212499) in 13.2.
+
Going a step further, GitLab can show the Code Quality report right
in the merge request widget area if a report from the target branch is available to compare to:
@@ -82,7 +87,7 @@ include:
The above example will create a `code_quality` job in your CI/CD pipeline which
will scan your source code for code quality issues. The report will be saved as a
-[Code Quality report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality-starter)
+[Code Quality report artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality)
that you can later download and analyze.
It's also possible to override the URL to the Code Quality image by
@@ -240,7 +245,7 @@ do this:
1. Define a job in your `.gitlab-ci.yml` file that generates the
[Code Quality report
- artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality-starter).
+ artifact](../../../ci/pipelines/job_artifacts.md#artifactsreportscodequality).
1. Configure your tool to generate the Code Quality report artifact as a JSON
file that implements a subset of the [Code Climate
spec](https://github.com/codeclimate/platform/blob/master/spec/analyzers/SPEC.md#data-types).
@@ -276,11 +281,11 @@ NOTE: **Note:**
Although the Code Climate spec supports more properties, those are ignored by
GitLab.
-## Code Quality reports
+## Code Quality reports **(STARTER)**
Once the Code Quality job has completed:
-- The full list of code quality violations generated by a pipeline is available in the
+- The full list of code quality violations generated by a pipeline is shown in the
Code Quality tab of the Pipeline Details page.
- Potential changes to code quality are shown directly in the merge request.
The Code Quality widget in the merge request compares the reports from the base and head of the branch,
@@ -293,7 +298,7 @@ Once the Code Quality job has completed:
### Using Analysis Plugins
-Should there be a need to extend the default functionality provided by Code Quality, as stated in [Code Quality](#code-quality-starter), [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) are available.
+Should there be a need to extend the default functionality provided by Code Quality, as stated in [Code Quality](#code-quality), [Analysis Plugins](https://docs.codeclimate.com/docs/list-of-engines) are available.
For example, to use the [SonarJava analyzer](https://docs.codeclimate.com/docs/sonar-java),
add a file named `.codeclimate.yml` containing the [enablement code](https://docs.codeclimate.com/docs/sonar-java#enable-the-plugin)
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 5d2813f5bfc..f68fc7d7b45 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -19,7 +19,7 @@ A. Consider you're a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team
-1. You work on the implementation optimizing code with [Code Quality reports](code_quality.md) **(STARTER)**
+1. You work on the implementation optimizing code with [Code Quality reports](code_quality.md)
1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD
1. You avoid using dependencies whose license is not compatible with your project with [License Compliance reports](../../compliance/license_compliance/index.md) **(ULTIMATE)**
1. You request the [approval](merge_request_approvals.md) from your manager **(STARTER)**
diff --git a/doc/user/project/merge_requests/testing_and_reports_in_merge_requests.md b/doc/user/project/merge_requests/testing_and_reports_in_merge_requests.md
index ab404d8d8a5..e5ebc46d58f 100644
--- a/doc/user/project/merge_requests/testing_and_reports_in_merge_requests.md
+++ b/doc/user/project/merge_requests/testing_and_reports_in_merge_requests.md
@@ -16,7 +16,7 @@ or link to useful information directly from merge requests:
| [Accessibility Testing](accessibility_testing.md) | Automatically report A11y violations for changed pages in merge requests. |
| [Browser Performance Testing](browser_performance_testing.md) **(PREMIUM)** | Quickly determine the browser performance impact of pending code changes. |
| [Load Performance Testing](load_performance_testing.md) **(PREMIUM)** | Quickly determine the server performance impact of pending code changes. |
-| [Code Quality](code_quality.md) **(STARTER)** | Analyze your source code quality using the [Code Climate](https://codeclimate.com/) analyzer and show the Code Climate report right in the merge request widget area. |
+| [Code Quality](code_quality.md) | Analyze your source code quality using the [Code Climate](https://codeclimate.com/) analyzer and show the Code Climate report right in the merge request widget area. |
| [Display arbitrary job artifacts](../../../ci/yaml/README.md#artifactsexpose_as) | Configure CI pipelines with the `artifacts:expose_as` parameter to directly link to selected [artifacts](../../../ci/pipelines/job_artifacts.md) in merge requests. |
| [GitLab CI/CD](../../../ci/README.md) | Build, test, and deploy your code in a per-branch basis with built-in CI/CD. |
| [JUnit test reports](../../../ci/junit_test_reports.md) | Configure your CI jobs to use JUnit test reports, and let GitLab display a report on the merge request so that it’s easier and faster to identify the failure without having to check the entire job log. |
diff --git a/lib/gitlab/jira_import/issue_serializer.rb b/lib/gitlab/jira_import/issue_serializer.rb
index df57680073e..43280606bb6 100644
--- a/lib/gitlab/jira_import/issue_serializer.rb
+++ b/lib/gitlab/jira_import/issue_serializer.rb
@@ -52,7 +52,9 @@ module Gitlab
end
def map_user_id(jira_user)
- Gitlab::JiraImport::UserMapper.new(project, jira_user).execute&.id
+ return unless jira_user&.dig('accountId')
+
+ Gitlab::JiraImport.get_user_mapping(project.id, jira_user['accountId'])
end
def reporter
diff --git a/lib/gitlab/jira_import/user_mapper.rb b/lib/gitlab/jira_import/user_mapper.rb
deleted file mode 100644
index 208ee49b724..00000000000
--- a/lib/gitlab/jira_import/user_mapper.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-# frozen_string_literal: true
-
-module Gitlab
- module JiraImport
- class UserMapper
- include ::Gitlab::Utils::StrongMemoize
-
- def initialize(project, jira_user)
- @project = project
- @jira_user = jira_user
- end
-
- def execute
- return unless jira_user
-
- email = jira_user['emailAddress']
-
- # We also include emails that are not yet confirmed
- users = User.by_any_email(email).to_a
-
- user = users.first
-
- # this event should never happen but we should log it in case we have invalid data
- log_user_mapping_message('Multiple users found for an email address', email) if users.count > 1
-
- unless project.project_member(user) || project.group&.group_member(user)
- log_user_mapping_message('Jira user not found', email)
-
- return
- end
-
- user
- end
-
- private
-
- attr_reader :project, :jira_user, :params
-
- def log_user_mapping_message(message, email)
- logger.info(
- project_id: project.id,
- project_path: project.full_path,
- user_email: email,
- message: message
- )
- end
-
- def logger
- @logger ||= Gitlab::Import::Logger.build
- end
- end
- end
-end
diff --git a/lib/gitlab/sidekiq_versioning.rb b/lib/gitlab/sidekiq_versioning.rb
index 8164a5a9d7a..0b0e090630a 100644
--- a/lib/gitlab/sidekiq_versioning.rb
+++ b/lib/gitlab/sidekiq_versioning.rb
@@ -5,6 +5,10 @@ module Gitlab
def self.install!
Sidekiq::Manager.prepend SidekiqVersioning::Manager
+ Sidekiq.server_middleware do |chain|
+ chain.add SidekiqVersioning::Middleware
+ end
+
# The Sidekiq client API always adds the queue to the Sidekiq queue
# list, but mail_room and gitlab-shell do not. This is only necessary
# for monitoring.
diff --git a/lib/gitlab/sidekiq_versioning/middleware.rb b/lib/gitlab/sidekiq_versioning/middleware.rb
new file mode 100644
index 00000000000..e5aa40722d2
--- /dev/null
+++ b/lib/gitlab/sidekiq_versioning/middleware.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqVersioning
+ class Middleware
+ def call(worker, job, queue)
+ worker.job_version = job['version']
+
+ yield
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/sidekiq_versioning/worker.rb b/lib/gitlab/sidekiq_versioning/worker.rb
new file mode 100644
index 00000000000..fe9bae6b8a1
--- /dev/null
+++ b/lib/gitlab/sidekiq_versioning/worker.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module SidekiqVersioning
+ module Worker
+ extend ActiveSupport::Concern
+
+ included do
+ version 0
+
+ attr_writer :job_version
+ end
+
+ class_methods do
+ def version(new_version = nil)
+ if new_version
+ sidekiq_options version: new_version.to_i
+ else
+ get_sidekiq_options['version']
+ end
+ end
+ end
+
+ # Version is not set if `new.perform` is called directly,
+ # and in that case we fallback to latest version
+ def job_version
+ @job_version ||= self.class.version
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 67148d60ecc..ee9f2a92766 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -12748,6 +12748,15 @@ msgstr ""
msgid "Integrations|Standard"
msgstr ""
+msgid "Integrations|This integration has multiple settings available."
+msgstr ""
+
+msgid "Integrations|Use custom settings"
+msgstr ""
+
+msgid "Integrations|Use instance level settings"
+msgstr ""
+
msgid "Integrations|When a Jira issue is mentioned in a commit or merge request a remote link and comment (if enabled) will be created."
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb
index fd342503a5c..41baaa02544 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Verify' do
+ RSpec.describe 'Verify', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/229724', type: :investigating } do
describe 'Add or Remove CI variable via UI', :smoke do
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index ea844008f64..e8a23dcfafb 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -179,6 +179,23 @@ RSpec.describe Projects::ServicesController do
it_behaves_like 'service update'
end
+
+ context 'wehn param `inherit_from_id` is set to empty string' do
+ let(:service_params) { { inherit_from_id: '' } }
+
+ it 'sets inherit_from_id to nil' do
+ expect(service.reload.inherit_from_id).to eq(nil)
+ end
+ end
+
+ context 'wehn param `inherit_from_id` is set to some value' do
+ let(:instance_service) { create(:jira_service, :instance) }
+ let(:service_params) { { inherit_from_id: instance_service.id } }
+
+ it 'sets inherit_from_id to value' do
+ expect(service.reload.inherit_from_id).to eq(instance_service.id)
+ end
+ end
end
describe 'as JSON' do
diff --git a/spec/features/projects/services/disable_triggers_spec.rb b/spec/features/projects/services/disable_triggers_spec.rb
index 8f87d0e7ff1..b3a3d7f0622 100644
--- a/spec/features/projects/services/disable_triggers_spec.rb
+++ b/spec/features/projects/services/disable_triggers_spec.rb
@@ -5,7 +5,7 @@ require 'spec_helper'
RSpec.describe 'Disable individual triggers', :js do
include_context 'project service activation'
- let(:checkbox_selector) { 'input[type=checkbox][name$="_events]"]' }
+ let(:checkbox_selector) { 'input[name$="_events]"]' }
before do
visit_project_integration(service_name)
@@ -18,7 +18,7 @@ RSpec.describe 'Disable individual triggers', :js do
event_count = HipchatService.supported_events.count
expect(page).to have_content "Trigger"
- expect(page).to have_css(checkbox_selector, count: event_count)
+ expect(page).to have_css(checkbox_selector, visible: :all, count: event_count)
end
end
@@ -27,7 +27,7 @@ RSpec.describe 'Disable individual triggers', :js do
it "doesn't show unnecessary Trigger checkboxes" do
expect(page).not_to have_content "Trigger"
- expect(page).not_to have_css(checkbox_selector)
+ expect(page).not_to have_css(checkbox_selector, visible: :all)
end
end
end
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index 0f34d6419d3..f1a7ac8b21a 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -6,7 +6,7 @@ import {
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
} from '~/blob/components/constants';
-import { GlButtonGroup, GlDeprecatedButton } from '@gitlab/ui';
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
describe('Blob Header Viewer Switcher', () => {
let wrapper;
@@ -35,7 +35,7 @@ describe('Blob Header Viewer Switcher', () => {
beforeEach(() => {
createComponent();
btnGroup = wrapper.find(GlButtonGroup);
- buttons = wrapper.findAll(GlDeprecatedButton);
+ buttons = wrapper.findAll(GlButton);
});
it('renders gl-button-group component', () => {
@@ -57,7 +57,7 @@ describe('Blob Header Viewer Switcher', () => {
function factory(propsData = {}) {
createComponent(propsData);
- buttons = wrapper.findAll(GlDeprecatedButton);
+ buttons = wrapper.findAll(GlButton);
simpleBtn = buttons.at(0);
richBtn = buttons.at(1);
diff --git a/spec/frontend/integrations/edit/components/active_toggle_spec.js b/spec/frontend/integrations/edit/components/active_toggle_spec.js
index 5469b45f708..228d8f5fc30 100644
--- a/spec/frontend/integrations/edit/components/active_toggle_spec.js
+++ b/spec/frontend/integrations/edit/components/active_toggle_spec.js
@@ -1,8 +1,10 @@
import { mount } from '@vue/test-utils';
-import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
import { GlToggle } from '@gitlab/ui';
+import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
+
const GL_TOGGLE_ACTIVE_CLASS = 'is-checked';
+const GL_TOGGLE_DISABLED_CLASS = 'is-disabled';
describe('ActiveToggle', () => {
let wrapper;
@@ -11,9 +13,12 @@ describe('ActiveToggle', () => {
initialActivated: true,
};
- const createComponent = props => {
+ const createComponent = (props = {}, isInheriting = false) => {
wrapper = mount(ActiveToggle, {
propsData: { ...defaultProps, ...props },
+ computed: {
+ isInheriting: () => isInheriting,
+ },
});
};
@@ -29,6 +34,15 @@ describe('ActiveToggle', () => {
const findInputInToggle = () => findGlToggle().find('input');
describe('template', () => {
+ describe('is inheriting adminSettings', () => {
+ it('renders GlToggle as disabled', () => {
+ createComponent({}, true);
+
+ expect(findGlToggle().exists()).toBe(true);
+ expect(findButtonInToggle().classes()).toContain(GL_TOGGLE_DISABLED_CLASS);
+ });
+ });
+
describe('initialActivated is false', () => {
it('renders GlToggle as inactive', () => {
createComponent({
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index 0a771c3b31d..3a7a0efcab7 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -14,9 +14,12 @@ describe('DynamicField', () => {
value: '1',
};
- const createComponent = props => {
+ const createComponent = (props, isInheriting = false) => {
wrapper = mount(DynamicField, {
propsData: { ...defaultProps, ...props },
+ computed: {
+ isInheriting: () => isInheriting,
+ },
});
};
@@ -34,108 +37,143 @@ describe('DynamicField', () => {
const findGlFormTextarea = () => wrapper.find(GlFormTextarea);
describe('template', () => {
- describe('dynamic field', () => {
- describe('type is checkbox', () => {
- beforeEach(() => {
- createComponent({
- type: 'checkbox',
+ describe.each([[true, 'disabled', 'readonly'], [false, undefined, undefined]])(
+ 'dynamic field, when isInheriting = `%p`',
+ (isInheriting, disabled, readonly) => {
+ describe('type is checkbox', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'checkbox',
+ },
+ isInheriting,
+ );
+ });
+
+ it(`renders GlFormCheckbox, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
+ expect(findGlFormCheckbox().exists()).toBe(true);
+ expect(
+ findGlFormCheckbox()
+ .find('[type=checkbox]')
+ .attributes('disabled'),
+ ).toBe(disabled);
+ });
+
+ it('does not render other types of input', () => {
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
});
});
- it('renders GlFormCheckbox', () => {
- expect(findGlFormCheckbox().exists()).toBe(true);
- });
+ describe('type is select', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'select',
+ choices: [['all', 'All details'], ['standard', 'Standard']],
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormSelect, which ${isInheriting ? 'is' : 'is not'} disabled`, () => {
+ expect(findGlFormSelect().exists()).toBe(true);
+ expect(findGlFormSelect().findAll('option')).toHaveLength(2);
+ expect(
+ findGlFormSelect()
+ .find('select')
+ .attributes('disabled'),
+ ).toBe(disabled);
+ });
- describe('type is select', () => {
- beforeEach(() => {
- createComponent({
- type: 'select',
- choices: [['all', 'All details'], ['standard', 'Standard']],
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
});
});
- it('renders findGlFormSelect', () => {
- expect(findGlFormSelect().exists()).toBe(true);
- expect(findGlFormSelect().findAll('option')).toHaveLength(2);
- });
+ describe('type is textarea', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'textarea',
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormTextarea, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormTextarea().exists()).toBe(true);
+ expect(
+ findGlFormTextarea()
+ .find('textarea')
+ .attributes('readonly'),
+ ).toBe(readonly);
+ });
- describe('type is textarea', () => {
- beforeEach(() => {
- createComponent({
- type: 'textarea',
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormInput().exists()).toBe(false);
});
});
- it('renders findGlFormTextarea', () => {
- expect(findGlFormTextarea().exists()).toBe(true);
- });
+ describe('type is password', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'password',
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormInput().exists()).toBe(false);
- });
- });
+ it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes('type')).toBe('password');
+ expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ });
- describe('type is password', () => {
- beforeEach(() => {
- createComponent({
- type: 'password',
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
});
});
- it('renders GlFormInput', () => {
- expect(findGlFormInput().exists()).toBe(true);
- expect(findGlFormInput().attributes('type')).toBe('password');
- });
+ describe('type is text', () => {
+ beforeEach(() => {
+ createComponent(
+ {
+ type: 'text',
+ required: true,
+ },
+ isInheriting,
+ );
+ });
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- });
- });
+ it(`renders GlFormInput, which ${isInheriting ? 'is' : 'is not'} readonly`, () => {
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().attributes()).toMatchObject({
+ type: 'text',
+ id: 'service_project_url',
+ name: 'service[project_url]',
+ placeholder: defaultProps.placeholder,
+ required: 'required',
+ });
+ expect(findGlFormInput().attributes('readonly')).toBe(readonly);
+ });
- describe('type is text', () => {
- beforeEach(() => {
- createComponent({
- type: 'text',
- required: true,
+ it('does not render other types of input', () => {
+ expect(findGlFormCheckbox().exists()).toBe(false);
+ expect(findGlFormSelect().exists()).toBe(false);
+ expect(findGlFormTextarea().exists()).toBe(false);
});
});
-
- it('renders GlFormInput', () => {
- expect(findGlFormInput().exists()).toBe(true);
- expect(findGlFormInput().attributes()).toMatchObject({
- type: 'text',
- id: 'service_project_url',
- name: 'service[project_url]',
- placeholder: defaultProps.placeholder,
- required: 'required',
- });
- });
-
- it('does not render other types of input', () => {
- expect(findGlFormCheckbox().exists()).toBe(false);
- expect(findGlFormSelect().exists()).toBe(false);
- expect(findGlFormTextarea().exists()).toBe(false);
- });
- });
- });
+ },
+ );
describe('help text', () => {
it('renders description with help text', () => {
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 3d27b9c3051..482c6a439f2 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,34 +1,29 @@
import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/integrations/edit/store';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
+import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import ActiveToggle from '~/integrations/edit/components/active_toggle.vue';
import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
+import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
describe('IntegrationForm', () => {
let wrapper;
- const defaultProps = {
- activeToggleProps: {
- initialActivated: true,
- },
- showActive: true,
- triggerFieldsProps: {
- initialTriggerCommit: false,
- initialTriggerMergeRequest: false,
- initialEnableComments: false,
- },
- jiraIssuesProps: {},
- type: '',
- };
-
- const createComponent = (props, featureFlags = {}) => {
+ const createComponent = (customStateProps = {}, featureFlags = {}, initialState = {}) => {
wrapper = shallowMount(IntegrationForm, {
- propsData: { ...defaultProps, ...props },
+ propsData: {},
+ store: createStore({
+ customState: { ...mockIntegrationProps, ...customStateProps },
+ ...initialState,
+ }),
stubs: {
+ OverrideDropdown,
ActiveToggle,
JiraTriggerFields,
+ TriggerFields,
},
provide: {
glFeatures: featureFlags,
@@ -43,6 +38,7 @@ describe('IntegrationForm', () => {
}
});
+ const findOverrideDropdown = () => wrapper.find(OverrideDropdown);
const findActiveToggle = () => wrapper.find(ActiveToggle);
const findJiraTriggerFields = () => wrapper.find(JiraTriggerFields);
const findJiraIssuesFields = () => wrapper.find(JiraIssuesFields);
@@ -140,5 +136,35 @@ describe('IntegrationForm', () => {
});
});
});
+
+ describe('adminState state is null', () => {
+ it('does not render OverrideDropdown', () => {
+ createComponent(
+ {},
+ {},
+ {
+ adminState: null,
+ },
+ );
+
+ expect(findOverrideDropdown().exists()).toBe(false);
+ });
+ });
+
+ describe('adminState state is an object', () => {
+ it('renders OverrideDropdown', () => {
+ createComponent(
+ {},
+ {},
+ {
+ adminState: {
+ ...mockIntegrationProps,
+ },
+ },
+ );
+
+ expect(findOverrideDropdown().exists()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index c89c54bd35d..f58825f6297 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -1,7 +1,9 @@
import { mount } from '@vue/test-utils';
-import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
+
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
+
describe('JiraIssuesFields', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
index e4c2a0be6a3..782930eb6a2 100644
--- a/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_trigger_fields_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
import { GlFormCheckbox } from '@gitlab/ui';
+import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_fields.vue';
describe('JiraTriggerFields', () => {
let wrapper;
@@ -11,9 +11,12 @@ describe('JiraTriggerFields', () => {
initialEnableComments: false,
};
- const createComponent = props => {
+ const createComponent = (props, isInheriting = false) => {
wrapper = mount(JiraTriggerFields, {
propsData: { ...defaultProps, ...props },
+ computed: {
+ isInheriting: () => isInheriting,
+ },
});
};
@@ -93,5 +96,23 @@ describe('JiraTriggerFields', () => {
expect(findCommentDetail().isVisible()).toBe(true);
});
});
+
+ it('disables checkboxes and radios if inheriting', () => {
+ createComponent(
+ {
+ initialTriggerCommit: true,
+ initialEnableComments: true,
+ },
+ true,
+ );
+
+ wrapper.findAll('[type=checkbox]').wrappers.forEach(checkbox => {
+ expect(checkbox.attributes('disabled')).toBe('disabled');
+ });
+
+ wrapper.findAll('[type=radio]').wrappers.forEach(radio => {
+ expect(radio.attributes('disabled')).toBe('disabled');
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index 337876c6d16..41bccb8ada0 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -9,9 +9,12 @@ describe('TriggerFields', () => {
type: 'slack',
};
- const createComponent = props => {
+ const createComponent = (props, isInheriting = false) => {
wrapper = mount(TriggerFields, {
propsData: { ...defaultProps, ...props },
+ computed: {
+ isInheriting: () => isInheriting,
+ },
});
};
@@ -22,10 +25,11 @@ describe('TriggerFields', () => {
}
});
+ const findAllGlFormGroups = () => wrapper.find('#trigger-fields').findAll(GlFormGroup);
const findAllGlFormCheckboxes = () => wrapper.findAll(GlFormCheckbox);
const findAllGlFormInputs = () => wrapper.findAll(GlFormInput);
- describe('template', () => {
+ describe.each([true, false])('template, isInheriting = `%p`', isInheriting => {
it('renders a label with text "Trigger"', () => {
createComponent();
@@ -51,9 +55,12 @@ describe('TriggerFields', () => {
];
beforeEach(() => {
- createComponent({
- events,
- });
+ createComponent(
+ {
+ events,
+ },
+ isInheriting,
+ );
});
it('does not render GlFormInput for each event', () => {
@@ -69,8 +76,10 @@ describe('TriggerFields', () => {
});
});
- it('renders GlFormCheckbox for each event', () => {
- const checkboxes = findAllGlFormCheckboxes();
+ it(`renders GlFormCheckbox and corresponding hidden input for each event, which ${
+ isInheriting ? 'is' : 'is not'
+ } disabled`, () => {
+ const checkboxes = findAllGlFormGroups();
const expectedResults = [
{ labelText: 'Push', inputName: 'service[push_event]' },
{ labelText: 'Merge Request', inputName: 'service[merge_requests_event]' },
@@ -78,14 +87,22 @@ describe('TriggerFields', () => {
expect(checkboxes).toHaveLength(2);
checkboxes.wrappers.forEach((checkbox, index) => {
+ const checkBox = checkbox.find(GlFormCheckbox);
+
expect(checkbox.find('label').text()).toBe(expectedResults[index].labelText);
- expect(checkbox.find('input').attributes('name')).toBe(expectedResults[index].inputName);
- expect(checkbox.vm.$attrs.checked).toBe(events[index].value);
+ expect(checkbox.find('[type=hidden]').attributes('name')).toBe(
+ expectedResults[index].inputName,
+ );
+ expect(checkbox.find('[type=hidden]').attributes('value')).toBe(
+ events[index].value.toString(),
+ );
+ expect(checkBox.vm.$attrs.disabled).toBe(isInheriting);
+ expect(checkBox.vm.$attrs.checked).toBe(events[index].value);
});
});
});
- describe('events with field property', () => {
+ describe('events with field property, isInheriting = `%p`', () => {
const events = [
{
field: {
@@ -102,16 +119,21 @@ describe('TriggerFields', () => {
];
beforeEach(() => {
- createComponent({
- events,
- });
+ createComponent(
+ {
+ events,
+ },
+ isInheriting,
+ );
});
it('renders GlFormCheckbox for each event', () => {
expect(findAllGlFormCheckboxes()).toHaveLength(2);
});
- it('renders GlFormInput for each event', () => {
+ it(`renders GlFormInput for each event, which ${
+ isInheriting ? 'is' : 'is not'
+ } readonly`, () => {
const fields = findAllGlFormInputs();
const expectedResults = [
{
@@ -128,6 +150,7 @@ describe('TriggerFields', () => {
fields.wrappers.forEach((field, index) => {
expect(field.attributes()).toMatchObject(expectedResults[index]);
+ expect(field.vm.$attrs.readonly).toBe(isInheriting);
expect(field.vm.$attrs.value).toBe(events[index].field.value);
});
});
diff --git a/spec/frontend/integrations/edit/mock_data.js b/spec/frontend/integrations/edit/mock_data.js
new file mode 100644
index 00000000000..da2758ec15c
--- /dev/null
+++ b/spec/frontend/integrations/edit/mock_data.js
@@ -0,0 +1,18 @@
+// eslint-disable-next-line import/prefer-default-export
+export const mockIntegrationProps = {
+ id: 25,
+ activeToggleProps: {
+ initialActivated: true,
+ },
+ showActive: true,
+ triggerFieldsProps: {
+ initialTriggerCommit: false,
+ initialTriggerMergeRequest: false,
+ initialEnableComments: false,
+ },
+ jiraIssuesProps: {},
+ triggerEvents: [],
+ fields: [],
+ type: '',
+ inheritFromId: 25,
+};
diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js
new file mode 100644
index 00000000000..c3ce6e51a3d
--- /dev/null
+++ b/spec/frontend/integrations/edit/store/actions_spec.js
@@ -0,0 +1,19 @@
+import createState from '~/integrations/edit/store/state';
+import { setOverride } from '~/integrations/edit/store/actions';
+import * as types from '~/integrations/edit/store/mutation_types';
+
+import testAction from 'helpers/vuex_action_helper';
+
+describe('Integration form store actions', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe('setOverride', () => {
+ it('should commit override mutation', () => {
+ return testAction(setOverride, true, state, [{ type: types.SET_OVERRIDE, payload: true }]);
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/store/getters_spec.js b/spec/frontend/integrations/edit/store/getters_spec.js
new file mode 100644
index 00000000000..700d36edaad
--- /dev/null
+++ b/spec/frontend/integrations/edit/store/getters_spec.js
@@ -0,0 +1,71 @@
+import { currentKey, isInheriting, propsSource } from '~/integrations/edit/store/getters';
+import createState from '~/integrations/edit/store/state';
+import { mockIntegrationProps } from '../mock_data';
+
+describe('Integration form store getters', () => {
+ let state;
+ const customState = { ...mockIntegrationProps, type: 'CustomState' };
+ const adminState = { ...mockIntegrationProps, type: 'AdminState' };
+
+ beforeEach(() => {
+ state = createState({ customState });
+ });
+
+ describe('isInheriting', () => {
+ describe('when adminState is null', () => {
+ it('returns false', () => {
+ expect(isInheriting(state)).toBe(false);
+ });
+ });
+
+ describe('when adminState is an object', () => {
+ beforeEach(() => {
+ state.adminState = adminState;
+ });
+
+ describe('when override is false', () => {
+ beforeEach(() => {
+ state.override = false;
+ });
+
+ it('returns false', () => {
+ expect(isInheriting(state)).toBe(true);
+ });
+ });
+
+ describe('when override is true', () => {
+ beforeEach(() => {
+ state.override = true;
+ });
+
+ it('returns true', () => {
+ expect(isInheriting(state)).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('propsSource', () => {
+ beforeEach(() => {
+ state.adminState = adminState;
+ });
+
+ it('equals adminState if inheriting', () => {
+ expect(propsSource(state, { isInheriting: true })).toEqual(adminState);
+ });
+
+ it('equals customState if not inheriting', () => {
+ expect(propsSource(state, { isInheriting: false })).toEqual(customState);
+ });
+ });
+
+ describe('currentKey', () => {
+ it('equals `admin` if inheriting', () => {
+ expect(currentKey(state, { isInheriting: true })).toEqual('admin');
+ });
+
+ it('equals `custom` if not inheriting', () => {
+ expect(currentKey(state, { isInheriting: false })).toEqual('custom');
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/store/mutations_spec.js b/spec/frontend/integrations/edit/store/mutations_spec.js
new file mode 100644
index 00000000000..4b733726d44
--- /dev/null
+++ b/spec/frontend/integrations/edit/store/mutations_spec.js
@@ -0,0 +1,19 @@
+import mutations from '~/integrations/edit/store/mutations';
+import createState from '~/integrations/edit/store/state';
+import * as types from '~/integrations/edit/store/mutation_types';
+
+describe('Integration form store mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState();
+ });
+
+ describe(`${types.SET_OVERRIDE}`, () => {
+ it('sets override', () => {
+ mutations[types.SET_OVERRIDE](state, true);
+
+ expect(state.override).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
new file mode 100644
index 00000000000..a8b431aa310
--- /dev/null
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -0,0 +1,26 @@
+import createState from '~/integrations/edit/store/state';
+
+describe('Integration form state factory', () => {
+ it('states default to null', () => {
+ expect(createState()).toEqual({
+ adminState: null,
+ customState: {},
+ override: false,
+ });
+ });
+
+ describe('override is initialized correctly', () => {
+ it.each([
+ [{ id: 25 }, { inheritFromId: null }, true],
+ [{ id: 25 }, { inheritFromId: 27 }, true],
+ [{ id: 25 }, { inheritFromId: 25 }, false],
+ [null, { inheritFromId: null }, false],
+ [null, { inheritFromId: 25 }, false],
+ ])(
+ 'for adminState: %p, customState: %p: override = `%p`',
+ (adminState, customState, expected) => {
+ expect(createState({ adminState, customState }).override).toEqual(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 1aaae80dcdf..2e52958a828 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -1,4 +1,4 @@
-import { insertMarkdownText, keypressNoteText } from '~/lib/utils/text_markdown';
+import { insertMarkdownText } from '~/lib/utils/text_markdown';
describe('init markdown', () => {
let textArea;
@@ -115,15 +115,14 @@ describe('init markdown', () => {
describe('with selection', () => {
const text = 'initial selected value';
const selected = 'selected';
- let selectedIndex;
-
beforeEach(() => {
textArea.value = text;
- selectedIndex = text.indexOf(selected);
+ const selectedIndex = text.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
});
it('applies the tag to the selected value', () => {
+ const selectedIndex = text.indexOf(selected);
const tag = '*';
insertMarkdownText({
@@ -154,29 +153,6 @@ describe('init markdown', () => {
expect(textArea.value).toEqual(text.replace(selected, `[${selected}](url)`));
});
- it.each`
- key | expected
- ${'['} | ${`[${selected}]`}
- ${'*'} | ${`**${selected}**`}
- ${"'"} | ${`'${selected}'`}
- ${'_'} | ${`_${selected}_`}
- ${'`'} | ${`\`${selected}\``}
- ${'"'} | ${`"${selected}"`}
- ${'{'} | ${`{${selected}}`}
- ${'('} | ${`(${selected})`}
- ${'<'} | ${`<${selected}>`}
- `('generates $expected when $key is pressed', ({ key, expected }) => {
- const event = new KeyboardEvent('keydown', { key });
-
- textArea.addEventListener('keydown', keypressNoteText);
- textArea.dispatchEvent(event);
-
- expect(textArea.value).toEqual(text.replace(selected, expected));
-
- // cursor placement should be after selection + 2 tag lengths
- expect(textArea.selectionStart).toBe(selectedIndex + expected.length);
- });
-
describe('and text to be selected', () => {
const tag = '[{text}](url)';
const select = 'url';
@@ -202,7 +178,7 @@ describe('init markdown', () => {
it('selects the right text when multiple tags are present', () => {
const initialValue = `${tag} ${tag} ${selected}`;
textArea.value = initialValue;
- selectedIndex = initialValue.indexOf(selected);
+ const selectedIndex = initialValue.indexOf(selected);
textArea.setSelectionRange(selectedIndex, selectedIndex + selected.length);
insertMarkdownText({
textArea,
@@ -228,7 +204,7 @@ describe('init markdown', () => {
const initialValue = `text ${expectedUrl} text`;
textArea.value = initialValue;
- selectedIndex = initialValue.indexOf(expectedUrl);
+ const selectedIndex = initialValue.indexOf(expectedUrl);
textArea.setSelectionRange(selectedIndex, selectedIndex + expectedUrl.length);
insertMarkdownText({
diff --git a/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
new file mode 100644
index 00000000000..3e11af9c9df
--- /dev/null
+++ b/spec/frontend/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -0,0 +1,62 @@
+import { shallowMount } from '@vue/test-utils';
+import component from '~/reports/codequality_report/components/codequality_issue_body.vue';
+import { STATUS_FAILED, STATUS_NEUTRAL, STATUS_SUCCESS } from '~/reports/constants';
+
+describe('code quality issue body issue body', () => {
+ let wrapper;
+
+ const codequalityIssue = {
+ name:
+ 'rubygem-rest-client: session fixation vulnerability via Set-Cookie headers in 30x redirection responses',
+ path: 'Gemfile.lock',
+ severity: 'normal',
+ type: 'Issue',
+ urlPath: '/Gemfile.lock#L22',
+ };
+
+ const mountWithStatus = initialStatus => {
+ wrapper = shallowMount(component, {
+ propsData: {
+ issue: codequalityIssue,
+ status: initialStatus,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('with success', () => {
+ it('renders fixed label', () => {
+ mountWithStatus(STATUS_SUCCESS);
+
+ expect(wrapper.text()).toContain('Fixed');
+ });
+ });
+
+ describe('without success', () => {
+ it('renders fixed label', () => {
+ mountWithStatus(STATUS_FAILED);
+
+ expect(wrapper.text()).not.toContain('Fixed');
+ });
+ });
+
+ describe('name', () => {
+ it('renders name', () => {
+ mountWithStatus(STATUS_NEUTRAL);
+
+ expect(wrapper.text()).toContain(codequalityIssue.name);
+ });
+ });
+
+ describe('path', () => {
+ it('renders the report-link path using the correct code quality issue', () => {
+ mountWithStatus(STATUS_NEUTRAL);
+
+ expect(wrapper.find('report-link-stub').props('issue')).toBe(codequalityIssue);
+ });
+ });
+});
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
new file mode 100644
index 00000000000..1905ca0d5e1
--- /dev/null
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -0,0 +1,146 @@
+import { mount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import GroupedCodequalityReportsApp from '~/reports/codequality_report/grouped_codequality_reports_app.vue';
+import CodequalityIssueBody from '~/reports/codequality_report/components/codequality_issue_body.vue';
+import store from '~/reports/codequality_report/store';
+import { mockParsedHeadIssues, mockParsedBaseIssues } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Grouped code quality reports app', () => {
+ const Component = localVue.extend(GroupedCodequalityReportsApp);
+ let wrapper;
+ let mockStore;
+
+ const mountComponent = (props = {}) => {
+ wrapper = mount(Component, {
+ store: mockStore,
+ localVue,
+ propsData: {
+ basePath: 'base.json',
+ headPath: 'head.json',
+ baseBlobPath: 'base/blob/path/',
+ headBlobPath: 'head/blob/path/',
+ codequalityHelpPath: 'codequality_help.html',
+ ...props,
+ },
+ methods: {
+ fetchReports: () => {},
+ },
+ });
+ };
+
+ const findWidget = () => wrapper.find('.js-codequality-widget');
+ const findIssueBody = () => wrapper.find(CodequalityIssueBody);
+
+ beforeEach(() => {
+ mockStore = store();
+ mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when it is loading reports', () => {
+ beforeEach(() => {
+ mockStore.state.isLoading = true;
+ });
+
+ it('should render loading text', () => {
+ expect(findWidget().text()).toEqual('Loading codeclimate report');
+ });
+ });
+
+ describe('when base and head reports are loaded and compared', () => {
+ describe('with no issues', () => {
+ beforeEach(() => {
+ mockStore.state.newIssues = [];
+ mockStore.state.resolvedIssues = [];
+ });
+
+ it('renders no changes text', () => {
+ expect(findWidget().text()).toEqual('No changes to code quality');
+ });
+ });
+
+ describe('with issues', () => {
+ describe('with new issues', () => {
+ beforeEach(() => {
+ mockStore.state.newIssues = [mockParsedHeadIssues[0]];
+ mockStore.state.resolvedIssues = [];
+ });
+
+ it('renders summary text', () => {
+ expect(findWidget().text()).toContain('Code quality degraded on 1 point');
+ });
+
+ it('renders custom codequality issue body', () => {
+ expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
+ });
+ });
+
+ describe('with resolved issues', () => {
+ beforeEach(() => {
+ mockStore.state.newIssues = [];
+ mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
+ });
+
+ it('renders summary text', () => {
+ expect(findWidget().text()).toContain('Code quality improved on 1 point');
+ });
+
+ it('renders custom codequality issue body', () => {
+ expect(findIssueBody().props('issue')).toEqual(mockParsedBaseIssues[0]);
+ });
+ });
+
+ describe('with new and resolved issues', () => {
+ beforeEach(() => {
+ mockStore.state.newIssues = [mockParsedHeadIssues[0]];
+ mockStore.state.resolvedIssues = [mockParsedBaseIssues[0]];
+ });
+
+ it('renders summary text', () => {
+ expect(findWidget().text()).toContain(
+ 'Code quality improved on 1 point and degraded on 1 point',
+ );
+ });
+
+ it('renders custom codequality issue body', () => {
+ expect(findIssueBody().props('issue')).toEqual(mockParsedHeadIssues[0]);
+ });
+ });
+ });
+ });
+
+ describe('when there is a head report but no base report', () => {
+ beforeEach(() => {
+ mockStore.state.basePath = null;
+ mockStore.state.hasError = true;
+ });
+
+ it('renders error text', () => {
+ expect(findWidget().text()).toEqual('Failed to load codeclimate report');
+ });
+
+ it('renders a help icon with more information', () => {
+ expect(findWidget().html()).toContain('ic-question');
+ });
+ });
+
+ describe('on error', () => {
+ beforeEach(() => {
+ mockStore.state.hasError = true;
+ });
+
+ it('renders error text', () => {
+ expect(findWidget().text()).toContain('Failed to load codeclimate report');
+ });
+
+ it('does not render a help icon', () => {
+ expect(findWidget().html()).not.toContain('ic-question');
+ });
+ });
+});
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index 18665da6b48..e00456a78b5 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -211,6 +211,15 @@ export default {
can_revert_on_current_merge_request: true,
can_cherry_pick_on_current_merge_request: true,
},
+ codeclimate: {
+ head_path: 'head.json',
+ base_path: 'base.json',
+ },
+ blob_path: {
+ base_path: 'blob_path',
+ head_path: 'blob_path',
+ },
+ codequality_help_path: 'code_quality.html',
target_branch_path: '/root/acets-app/branches/master',
source_branch_path: '/root/acets-app/branches/daaaa',
conflict_resolution_ui_path: '/root/acets-app/-/merge_requests/22/conflicts',
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index e022f68fdec..93659fa54fb 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -609,6 +609,12 @@ describe('mrWidgetOptions', () => {
});
});
+ describe('code quality widget', () => {
+ it('renders the component', () => {
+ expect(vm.$el.querySelector('.js-codequality-widget')).toExist();
+ });
+ });
+
describe('pipeline for target branch after merge', () => {
describe('with information for target branch pipeline', () => {
beforeEach(done => {
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index e54cd345a37..1cb2c6c669b 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -49,14 +49,18 @@ describe('getStateKey', () => {
expect(bound()).toEqual('unresolvedDiscussions');
+ data.work_in_progress = true;
+
+ expect(bound()).toEqual('workInProgress');
+
context.onlyAllowMergeIfPipelineSucceeds = true;
context.isPipelineFailed = true;
expect(bound()).toEqual('pipelineFailed');
- data.work_in_progress = true;
+ context.shouldBeRebased = true;
- expect(bound()).toEqual('workInProgress');
+ expect(bound()).toEqual('rebase');
data.has_conflicts = true;
diff --git a/spec/helpers/services_helper_spec.rb b/spec/helpers/services_helper_spec.rb
index 37256c1b387..10d6ec7b6a7 100644
--- a/spec/helpers/services_helper_spec.rb
+++ b/spec/helpers/services_helper_spec.rb
@@ -7,4 +7,28 @@ RSpec.describe ServicesHelper do
it { expect(event_action_title('comment')).to eq 'Comment' }
it { expect(event_action_title('something')).to eq 'Something' }
end
+
+ describe '#integration_form_data' do
+ subject { helper.integration_form_data(integration) }
+
+ context 'Jira service' do
+ let(:integration) { build(:jira_service) }
+
+ it 'includes Jira specific fields' do
+ is_expected.to include(
+ :id,
+ :show_active,
+ :activated,
+ :type,
+ :merge_request_events,
+ :commit_events,
+ :enable_comments,
+ :comment_detail,
+ :trigger_events,
+ :fields,
+ :inherit_from_id
+ )
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
index 684040f4e4b..4adc4e4d22a 100644
--- a/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
+++ b/spec/lib/gitlab/jira_import/issue_serializer_spec.rb
@@ -10,6 +10,7 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
let_it_be(:other_project_label) { create(:label, project: project, title: 'feature') }
let_it_be(:group_label) { create(:group_label, group: group, title: 'dev') }
let_it_be(:current_user) { create(:user) }
+ let_it_be(:user) { create(:user) }
let(:iid) { 5 }
let(:key) { 'PROJECT-5' }
@@ -17,8 +18,8 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
let(:description) { 'basic description' }
let(:created_at) { '2020-01-01 20:00:00' }
let(:updated_at) { '2020-01-10 20:00:00' }
- let(:assignee) { double(attrs: { 'displayName' => 'Solver', 'emailAddress' => 'assignee@example.com' }) }
- let(:reporter) { double(attrs: { 'displayName' => 'Reporter', 'emailAddress' => 'reporter@example.com' }) }
+ let(:assignee) { nil }
+ let(:reporter) { nil }
let(:jira_status) { 'new' }
let(:parent_field) do
@@ -109,11 +110,12 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
end
context 'author' do
- context 'when reporter maps to a valid GitLab user' do
- let!(:user) { create(:user, email: 'reporter@example.com') }
+ let(:reporter) { double(attrs: { 'displayName' => 'Solver', 'accountId' => 'abcd' }) }
+ context 'when reporter maps to a valid GitLab user' do
it 'sets the issue author to the mapped user' do
- project.add_developer(user)
+ expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, 'abcd')
+ .and_return(user.id)
expect(subject[:author_id]).to eq(user.id)
end
@@ -121,6 +123,9 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
context 'when reporter does not map to a valid Gitlab user' do
it 'defaults the issue author to project creator' do
+ expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, 'abcd')
+ .and_return(nil)
+
expect(subject[:author_id]).to eq(current_user.id)
end
end
@@ -129,25 +134,30 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
let(:reporter) { nil }
it 'defaults the issue author to project creator' do
+ expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
+
expect(subject[:author_id]).to eq(current_user.id)
end
end
- context 'when reporter field is missing email address' do
+ context 'when reporter field is missing accountId' do
let(:reporter) { double(attrs: { 'displayName' => 'Reporter' }) }
it 'defaults the issue author to project creator' do
+ expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
+
expect(subject[:author_id]).to eq(current_user.id)
end
end
end
context 'assignee' do
- context 'when assignee maps to a valid GitLab user' do
- let!(:user) { create(:user, email: 'assignee@example.com') }
+ let(:assignee) { double(attrs: { 'displayName' => 'Solver', 'accountId' => '1234' }) }
+ context 'when assignee maps to a valid GitLab user' do
it 'sets the issue assignees to the mapped user' do
- project.add_developer(user)
+ expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, '1234')
+ .and_return(user.id)
expect(subject[:assignee_ids]).to eq([user.id])
end
@@ -155,6 +165,9 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
context 'when assignee does not map to a valid GitLab user' do
it 'leaves the assignee empty' do
+ expect(Gitlab::JiraImport).to receive(:get_user_mapping).with(project.id, '1234')
+ .and_return(nil)
+
expect(subject[:assignee_ids]).to be_nil
end
end
@@ -163,14 +176,18 @@ RSpec.describe Gitlab::JiraImport::IssueSerializer do
let(:assignee) { nil }
it 'leaves the assignee empty' do
+ expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
+
expect(subject[:assignee_ids]).to be_nil
end
end
- context 'when assginee field is missing email address' do
- let(:assignee) { double(attrs: { 'displayName' => 'Reporter' }) }
+ context 'when assginee field is missing accountId' do
+ let(:assignee) { double(attrs: { 'displayName' => 'Solver' }) }
it 'leaves the assignee empty' do
+ expect(Gitlab::JiraImport).not_to receive(:get_user_mapping)
+
expect(subject[:assignee_ids]).to be_nil
end
end
diff --git a/spec/lib/gitlab/jira_import/user_mapper_spec.rb b/spec/lib/gitlab/jira_import/user_mapper_spec.rb
deleted file mode 100644
index 576667caeca..00000000000
--- a/spec/lib/gitlab/jira_import/user_mapper_spec.rb
+++ /dev/null
@@ -1,80 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Gitlab::JiraImport::UserMapper do
- let_it_be(:group) { create(:group) }
- let_it_be(:project) { create(:project, group: group) }
- let_it_be(:user) { create(:user, email: 'user@example.com') }
- let_it_be(:email) { create(:email, user: user, email: 'second_email@example.com', confirmed_at: nil) }
-
- let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'user@example.com' } }
-
- describe '#execute' do
- subject { described_class.new(project, jira_user).execute }
-
- context 'when jira_user is nil' do
- let(:jira_user) { nil }
-
- it 'returns nil' do
- expect(subject).to be_nil
- end
- end
-
- context 'when Gitlab user is not found by email' do
- let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'other@example.com' } }
-
- it 'returns nil' do
- expect(subject).to be_nil
- end
- end
-
- context 'when jira_user emailAddress is nil' do
- let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => nil } }
-
- it 'returns nil' do
- expect(subject).to be_nil
- end
- end
-
- context 'when jira_user emailAddress key is missing' do
- let(:jira_user) { { 'acountId' => '1a2b' } }
-
- it 'returns nil' do
- expect(subject).to be_nil
- end
- end
-
- context 'when found user is not a project member' do
- it 'returns nil' do
- expect(subject).to be_nil
- end
- end
-
- context 'when found user is a project member' do
- it 'returns the found user' do
- project.add_developer(user)
-
- expect(subject).to eq(user)
- end
- end
-
- context 'when user found by unconfirmd secondary address is a project member' do
- let(:jira_user) { { 'acountId' => '1a2b', 'emailAddress' => 'second_email@example.com' } }
-
- it 'returns the found user' do
- project.add_developer(user)
-
- expect(subject).to eq(user)
- end
- end
-
- context 'when user is a group member' do
- it 'returns the found user' do
- group.add_developer(user)
-
- expect(subject).to eq(user)
- end
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_versioning/middleware_spec.rb b/spec/lib/gitlab/sidekiq_versioning/middleware_spec.rb
new file mode 100644
index 00000000000..d054e034250
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_versioning/middleware_spec.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SidekiqVersioning::Middleware do
+ let(:worker_class) do
+ Class.new do
+ def self.name
+ 'DummyWorker'
+ end
+
+ include ApplicationWorker
+
+ version 2
+ end
+ end
+
+ describe '#call' do
+ let(:worker) { worker_class.new }
+ let(:job) { { 'version' => 3, 'queue' => queue } }
+ let(:queue) { worker_class.queue }
+
+ def call!(&block)
+ block ||= -> {}
+ subject.call(worker, job, queue, &block)
+ end
+
+ it 'sets worker.job_version' do
+ call!
+
+ expect(worker.job_version).to eq(job['version'])
+ end
+
+ it 'yields' do
+ expect { |b| call!(&b) }.to yield_control
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_versioning/worker_spec.rb b/spec/lib/gitlab/sidekiq_versioning/worker_spec.rb
new file mode 100644
index 00000000000..0781c5100fd
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_versioning/worker_spec.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::SidekiqVersioning::Worker do
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'DummyWorker'
+ end
+
+ # ApplicationWorker includes Gitlab::SidekiqVersioning::Worker
+ include ApplicationWorker
+
+ version 2
+ end
+ end
+
+ describe '.version' do
+ context 'when called with an argument' do
+ it 'sets the version option' do
+ worker.version 3
+
+ expect(worker.get_sidekiq_options['version']).to eq(3)
+ end
+ end
+
+ context 'when called without an argument' do
+ it 'returns the version option' do
+ worker.sidekiq_options version: 3
+
+ expect(worker.version).to eq(3)
+ end
+ end
+ end
+
+ describe '#job_version' do
+ let(:job) { worker.new }
+
+ context 'when job_version is not set' do
+ it 'returns latest version' do
+ expect(job.job_version).to eq(2)
+ end
+ end
+
+ context 'when job_version is set' do
+ it 'returns the set version' do
+ job.job_version = 0
+
+ expect(job.job_version).to eq(0)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sidekiq_versioning_spec.rb b/spec/lib/gitlab/sidekiq_versioning_spec.rb
index ed9650fc166..9599f2deed6 100644
--- a/spec/lib/gitlab/sidekiq_versioning_spec.rb
+++ b/spec/lib/gitlab/sidekiq_versioning_spec.rb
@@ -35,6 +35,12 @@ RSpec.describe Gitlab::SidekiqVersioning, :redis do
expect(Sidekiq::Manager).to include(Gitlab::SidekiqVersioning::Manager)
end
+ it 'adds the SidekiqVersioning::Middleware Sidekiq server middleware' do
+ described_class.install!
+
+ expect(Sidekiq.server_middleware.entries.map(&:klass)).to include(Gitlab::SidekiqVersioning::Middleware)
+ end
+
it 'registers all versionless and versioned queues with Redis' do
described_class.install!
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 8c914c6b74c..75bbb074526 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -386,6 +386,33 @@ RSpec.describe Service do
end
end
+ describe 'instance' do
+ describe '.instance_for' do
+ let_it_be(:jira_service) { create(:jira_service, :instance) }
+ let_it_be(:slack_service) { create(:slack_service, :instance) }
+
+ subject { described_class.instance_for(type) }
+
+ context 'Hipchat serivce' do
+ let(:type) { 'HipchatService' }
+
+ it { is_expected.to eq(nil) }
+ end
+
+ context 'Jira serivce' do
+ let(:type) { 'JiraService' }
+
+ it { is_expected.to eq(jira_service) }
+ end
+
+ context 'Slack serivce' do
+ let(:type) { 'SlackService' }
+
+ it { is_expected.to eq(slack_service) }
+ end
+ end
+ end
+
describe "{property}_changed?" do
let(:service) do
BambooService.create(