-
+
{{ s__('mrWidget|Members who can merge are allowed to add commits.') }}
diff --git a/app/controllers/projects/security/configuration_controller.rb b/app/controllers/projects/security/configuration_controller.rb
index 19de157357a..3a473bb67e0 100644
--- a/app/controllers/projects/security/configuration_controller.rb
+++ b/app/controllers/projects/security/configuration_controller.rb
@@ -7,6 +7,10 @@ module Projects
feature_category :static_application_security_testing
+ before_action only: [:show] do
+ push_frontend_feature_flag(:security_configuration_redesign, project, default_enabled: :yaml)
+ end
+
def show
render_403 unless can?(current_user, :read_security_configuration, project)
end
diff --git a/app/finders/security/jobs_finder.rb b/app/finders/security/jobs_finder.rb
index e2efb2e18c9..b8649520c20 100644
--- a/app/finders/security/jobs_finder.rb
+++ b/app/finders/security/jobs_finder.rb
@@ -15,7 +15,7 @@ module Security
attr_reader :pipeline
def self.allowed_job_types
- # Example return: [:sast, :dast, :dependency_scanning, :container_scanning, :license_management, :coverage_fuzzing]
+ # Example return: [:sast, :dast, :dependency_scanning, :container_scanning, :license_scanning, :coverage_fuzzing]
raise NotImplementedError, 'allowed_job_types must be overwritten to return an array of job types'
end
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 98af69d43b7..82c0df354d4 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -17,7 +17,7 @@
= link_to _('Forgot your password?'), new_password_path(:user)
%div
- if captcha_enabled? || captcha_on_login_required?
- = recaptcha_tags
+ = recaptcha_tags nonce: content_security_policy_nonce
.submit-container.move-submit-down
= f.submit _('Sign in'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'sign_in_button' }
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 4c70d8bf6f6..fd40188ac03 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -9,7 +9,7 @@
.devise-errors
= render 'devise/shared/error_messages', resource: resource
- if Gitlab::CurrentSettings.invisible_captcha_enabled
- = invisible_captcha
+ = invisible_captcha nonce: true
.name.form-row
.col.form-group
= f.label :first_name, _('First name'), for: 'new_user_first_name', class: 'label-bold'
@@ -59,7 +59,7 @@
%p.gl-field-hint.text-secondary= s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
%div
- if show_recaptcha_sign_up?
- = recaptcha_tags
+ = recaptcha_tags nonce: content_security_policy_nonce
.submit-container
= f.submit button_text, class: 'btn gl-button btn-confirm', data: { qa_selector: 'new_user_register_button' }
= render 'devise/shared/terms_of_service_notice', button_text: button_text
diff --git a/app/views/groups/_new_group_fields.html.haml b/app/views/groups/_new_group_fields.html.haml
index 0e3309fc8da..49c8c2700ce 100644
--- a/app/views/groups/_new_group_fields.html.haml
+++ b/app/views/groups/_new_group_fields.html.haml
@@ -20,7 +20,7 @@
- if captcha_required?
.row.recaptcha
.col-sm-4
- = recaptcha_tags
+ = recaptcha_tags nonce: content_security_policy_nonce
.row
.col-sm-12
= f.submit _('Create group'), class: "btn gl-button btn-confirm"
diff --git a/app/views/shared/_recaptcha_form.html.haml b/app/views/shared/_recaptcha_form.html.haml
index 5c5fc714aea..ae0a22fd255 100644
--- a/app/views/shared/_recaptcha_form.html.haml
+++ b/app/views/shared/_recaptcha_form.html.haml
@@ -10,7 +10,7 @@
= hidden_field(resource_name, field, value: value)
= hidden_field_tag(:spam_log_id, spammable.spam_log.id)
-# The reCAPTCHA response value will be returned in the 'g-recaptcha-response' field
- = recaptcha_tags script: script, callback: 'recaptchaDialogCallback' unless Rails.env.test?
+ = recaptcha_tags script: script, callback: 'recaptchaDialogCallback', nonce: content_security_policy_nonce unless Rails.env.test?
-# Fake the 'g-recaptcha-response' field in the test environment, so that the feature spec
-# can get to the (mocked) SpamVerdictService check.
= hidden_field_tag('g-recaptcha-response', 'abc123') if Rails.env.test?
diff --git a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
index 2e4e2dd3232..7ce8e23f464 100644
--- a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
@@ -40,7 +40,9 @@ module AuthorizedProjectUpdate
private
def use_primary_database
- # no-op in CE, overriden in EE
+ if ::Gitlab::Database::LoadBalancing.enable?
+ ::Gitlab::Database::LoadBalancing::Session.current.use_primary!
+ end
end
def project_authorizations_needs_refresh?(user)
@@ -54,5 +56,3 @@ module AuthorizedProjectUpdate
end
end
end
-
-AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker.prepend_mod_with('AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker')
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 2c2922d6fae..ad2cecc2f9b 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -15,10 +15,8 @@
- audit_reports
- authentication_and_authorization
- auto_devops
-- auto_portfolio_mgmt
- backup_restore
- boards
-- browser_performance
- chatops
- cloud_native_installation
- cluster_cost_management
@@ -78,7 +76,6 @@
- license
- license_compliance
- live_preview
-- load_testing
- logging
- memory
- merge_trains
@@ -87,9 +84,11 @@
- mobile_signing_deployment
- navigation
- omnibus_package
+- on_call_schedule_management
- onboarding
- package_registry
- pages
+- performance_testing
- pipeline_authoring
- planning_analytics
- privacy_control_center
@@ -111,6 +110,7 @@
- self_monitoring
- serverless
- service_desk
+- sharding
- snippets
- source_code_management
- static_application_security_testing
@@ -130,3 +130,4 @@
- web_firewall
- web_ide
- wiki
+- workflow_automation
diff --git a/config/feature_flags/development/security_configuration_redesign.yml b/config/feature_flags/development/security_configuration_redesign.yml
new file mode 100644
index 00000000000..16627191edc
--- /dev/null
+++ b/config/feature_flags/development/security_configuration_redesign.yml
@@ -0,0 +1,8 @@
+---
+name: security_configuration_redesign
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62285
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/331614
+milestone: '14.0'
+type: development
+group: group::static analysis
+default_enabled: false
diff --git a/db/post_migrate/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb b/db/post_migrate/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb
new file mode 100644
index 00000000000..7d4d97acf58
--- /dev/null
+++ b/db/post_migrate/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'UpdateJiraTrackerDataDeploymentTypeBasedOnUrl'
+ DELAY_INTERVAL = 2.minutes.to_i
+ BATCH_SIZE = 2_500
+
+ disable_ddl_transaction!
+
+ def up
+ say "Scheduling #{MIGRATION} jobs"
+ queue_background_migration_jobs_by_range_at_intervals(
+ define_batchable_model('jira_tracker_data'),
+ MIGRATION,
+ DELAY_INTERVAL,
+ batch_size: BATCH_SIZE
+ )
+ end
+
+ def down
+ # no-op
+ end
+end
diff --git a/db/post_migrate/20210526160133_remove_segment_selections_table.rb b/db/post_migrate/20210526160133_remove_segment_selections_table.rb
new file mode 100644
index 00000000000..02e981b40ba
--- /dev/null
+++ b/db/post_migrate/20210526160133_remove_segment_selections_table.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+class RemoveSegmentSelectionsTable < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ drop_table :analytics_devops_adoption_segment_selections
+ end
+
+ def down
+ create_table :analytics_devops_adoption_segment_selections do |t|
+ t.references :segment, index: { name: 'index_on_segment_selections_segment_id' }, null: false, foreign_key: { to_table: :analytics_devops_adoption_segments, on_delete: :cascade }
+ t.bigint :group_id
+ t.bigint :project_id
+ t.index [:group_id, :segment_id], unique: true, name: 'index_on_segment_selections_group_id_segment_id'
+ t.index [:project_id, :segment_id], unique: true, name: 'index_on_segment_selections_project_id_segment_id'
+
+ t.timestamps_with_timezone
+ end
+ add_concurrent_foreign_key(:analytics_devops_adoption_segment_selections, :projects, column: :project_id, on_delete: :cascade)
+ add_concurrent_foreign_key(:analytics_devops_adoption_segment_selections, :namespaces, column: :group_id, on_delete: :cascade)
+ add_check_constraint :analytics_devops_adoption_segment_selections, '(project_id != NULL AND group_id IS NULL) OR (group_id != NULL AND project_id IS NULL)', 'segment_selection_project_id_or_group_id_required'
+ end
+end
diff --git a/db/schema_migrations/20210421163509 b/db/schema_migrations/20210421163509
new file mode 100644
index 00000000000..b31e8209bbe
--- /dev/null
+++ b/db/schema_migrations/20210421163509
@@ -0,0 +1 @@
+0f6019cc094481cafbf0c9bd42f53ae09034ea87e3f360b02f9ec03192caab9d
\ No newline at end of file
diff --git a/db/schema_migrations/20210526160133 b/db/schema_migrations/20210526160133
new file mode 100644
index 00000000000..c61cc233c0e
--- /dev/null
+++ b/db/schema_migrations/20210526160133
@@ -0,0 +1 @@
+ee76ee2e2515c06b09fca23a77bdfb9532fa5d80fc3d5aba44a80d123b74cfa9
\ No newline at end of file
diff --git a/db/structure.sql b/db/structure.sql
index 610dc3b81ac..40eec742ac1 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -9110,25 +9110,6 @@ CREATE SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq
ALTER SEQUENCE analytics_cycle_analytics_project_value_streams_id_seq OWNED BY analytics_cycle_analytics_project_value_streams.id;
-CREATE TABLE analytics_devops_adoption_segment_selections (
- id bigint NOT NULL,
- segment_id bigint NOT NULL,
- group_id bigint,
- project_id bigint,
- created_at timestamp with time zone NOT NULL,
- updated_at timestamp with time zone NOT NULL,
- CONSTRAINT segment_selection_project_id_or_group_id_required CHECK ((((project_id <> NULL::bigint) AND (group_id IS NULL)) OR ((group_id <> NULL::bigint) AND (project_id IS NULL))))
-);
-
-CREATE SEQUENCE analytics_devops_adoption_segment_selections_id_seq
- START WITH 1
- INCREMENT BY 1
- NO MINVALUE
- NO MAXVALUE
- CACHE 1;
-
-ALTER SEQUENCE analytics_devops_adoption_segment_selections_id_seq OWNED BY analytics_devops_adoption_segment_selections.id;
-
CREATE TABLE analytics_devops_adoption_segments (
id bigint NOT NULL,
last_recorded_at timestamp with time zone,
@@ -19520,8 +19501,6 @@ ALTER TABLE ONLY analytics_cycle_analytics_project_stages ALTER COLUMN id SET DE
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams ALTER COLUMN id SET DEFAULT nextval('analytics_cycle_analytics_project_value_streams_id_seq'::regclass);
-ALTER TABLE ONLY analytics_devops_adoption_segment_selections ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segment_selections_id_seq'::regclass);
-
ALTER TABLE ONLY analytics_devops_adoption_segments ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_segments_id_seq'::regclass);
ALTER TABLE ONLY analytics_devops_adoption_snapshots ALTER COLUMN id SET DEFAULT nextval('analytics_devops_adoption_snapshots_id_seq'::regclass);
@@ -20611,9 +20590,6 @@ ALTER TABLE ONLY analytics_cycle_analytics_project_stages
ALTER TABLE ONLY analytics_cycle_analytics_project_value_streams
ADD CONSTRAINT analytics_cycle_analytics_project_value_streams_pkey PRIMARY KEY (id);
-ALTER TABLE ONLY analytics_devops_adoption_segment_selections
- ADD CONSTRAINT analytics_devops_adoption_segment_selections_pkey PRIMARY KEY (id);
-
ALTER TABLE ONLY analytics_devops_adoption_segments
ADD CONSTRAINT analytics_devops_adoption_segments_pkey PRIMARY KEY (id);
@@ -23899,12 +23875,6 @@ CREATE INDEX index_on_projects_lower_path ON projects USING btree (lower((path):
CREATE INDEX index_on_routes_lower_path ON routes USING btree (lower((path)::text));
-CREATE UNIQUE INDEX index_on_segment_selections_group_id_segment_id ON analytics_devops_adoption_segment_selections USING btree (group_id, segment_id);
-
-CREATE UNIQUE INDEX index_on_segment_selections_project_id_segment_id ON analytics_devops_adoption_segment_selections USING btree (project_id, segment_id);
-
-CREATE INDEX index_on_segment_selections_segment_id ON analytics_devops_adoption_segment_selections USING btree (segment_id);
-
CREATE INDEX index_on_snapshots_segment_id_end_time ON analytics_devops_adoption_snapshots USING btree (segment_id, end_time);
CREATE INDEX index_on_snapshots_segment_id_recorded_at ON analytics_devops_adoption_snapshots USING btree (segment_id, recorded_at);
@@ -25955,9 +25925,6 @@ ALTER TABLE ONLY project_group_links
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_dccd3f98fc FOREIGN KEY (assignee_id) REFERENCES users(id) ON DELETE SET NULL;
-ALTER TABLE ONLY analytics_devops_adoption_segment_selections
- ADD CONSTRAINT fk_ded7fe0344 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY issues
ADD CONSTRAINT fk_df75a7c8b8 FOREIGN KEY (promoted_to_epic_id) REFERENCES epics(id) ON DELETE SET NULL;
@@ -26027,9 +25994,6 @@ ALTER TABLE ONLY vulnerability_external_issue_links
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_f081aa4489 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
-ALTER TABLE ONLY analytics_devops_adoption_segment_selections
- ADD CONSTRAINT fk_f1472b95f3 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY boards
ADD CONSTRAINT fk_f15266b5f9 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
@@ -26120,9 +26084,6 @@ ALTER TABLE ONLY ip_restrictions
ALTER TABLE ONLY terraform_state_versions
ADD CONSTRAINT fk_rails_04f176e239 FOREIGN KEY (terraform_state_id) REFERENCES terraform_states(id) ON DELETE CASCADE;
-ALTER TABLE ONLY analytics_devops_adoption_segment_selections
- ADD CONSTRAINT fk_rails_053f00a9da FOREIGN KEY (segment_id) REFERENCES analytics_devops_adoption_segments(id) ON DELETE CASCADE;
-
ALTER TABLE ONLY ci_build_report_results
ADD CONSTRAINT fk_rails_056d298d48 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
index fe4d991689a..56af5f56cfa 100644
--- a/doc/administration/incoming_email.md
+++ b/doc/administration/incoming_email.md
@@ -21,9 +21,8 @@ GitLab has several features based on receiving incoming emails:
## Requirements
-It is **not** recommended to use an email address that receives any
-messages not intended for the GitLab instance. Any incoming emails not intended
-for GitLab receive a reject notice.
+We recommend using an email address that receives **only** messages that are intended for
+the GitLab instance. Any incoming emails not intended for GitLab receive a reject notice.
Handling incoming emails requires an [IMAP](https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol)-enabled
email account. GitLab requires one of the following three strategies:
@@ -131,6 +130,9 @@ list.
```shell
sudo gitlab-ctl reconfigure
+
+ # Needed when enabling or disabling for the first time but not for password changes.
+ # See https://gitlab.com/gitlab-org/gitlab-foss/-/issues/23560#note_61966788
sudo gitlab-ctl restart
```
diff --git a/doc/administration/redis/replication_and_failover_external.md b/doc/administration/redis/replication_and_failover_external.md
index 2716d9bba37..1d60e027157 100644
--- a/doc/administration/redis/replication_and_failover_external.md
+++ b/doc/administration/redis/replication_and_failover_external.md
@@ -21,7 +21,7 @@ The following are the requirements for providing your own Redis instance:
[requirements page](../../install/requirements.md).
- Standalone Redis or Redis high availability with Sentinel are supported. Redis
Cluster is not supported.
-- Managed Redis from cloud providers such as AWS ElastiCache will work. If these
+- Managed Redis from cloud providers such as AWS ElastiCache works fine. If these
services support high availability, be sure it is **not** the Redis Cluster type.
Note the Redis node's IP address or hostname, port, and password (if required).
@@ -53,7 +53,7 @@ Note the Redis node's IP address or hostname, port, and password (if required).
This is the documentation for configuring a scalable Redis setup when
you have installed Redis all by yourself and not using the bundled one that
comes with the Omnibus packages, although using the Omnibus GitLab packages is
-highly recommend as we optimize them specifically for GitLab, and we will take
+highly recommend as we optimize them specifically for GitLab, and we take
care of upgrading Redis to the latest supported version.
Note also that you may elect to override all references to
@@ -76,7 +76,7 @@ requirements:
(e.g., one from an internal network).
- Since Redis 3.2, you must define a password to receive external connections
(`requirepass`).
-- If you are using Redis with Sentinel, you will also need to define the same
+- If you are using Redis with Sentinel, you also need to define the same
password for the replica password definition (`masterauth`) in the same instance.
In addition, read the prerequisites as described in the
@@ -176,7 +176,7 @@ primary with IP `10.0.0.1` (some settings might overlap with the primary):
sentinel monitor gitlab-redis 10.0.0.1 6379 2
## Define with `sentinel down-after-milliseconds` the time in `ms`
- ## that an unresponsive server will be considered down.
+ ## that an unresponsive server is considered down.
sentinel down-after-milliseconds gitlab-redis 10000
## Define a value for `sentinel failover_timeout` in `ms`. This has multiple
@@ -197,7 +197,7 @@ primary with IP `10.0.0.1` (some settings might overlap with the primary):
##
## * The maximum time a failover in progress waits for all the replicas to be
## reconfigured as replicas of the new primary. However even after this time
- ## the replicas will be reconfigured by the Sentinels anyway, but not with
+ ## the replicas are reconfigured by the Sentinels anyway, but not with
## the exact parallel-syncs progression as specified.
sentinel failover_timeout 30000
```
@@ -249,7 +249,7 @@ In a real world usage, you would also set up firewall rules to prevent
unauthorized access from other machines, and block traffic from the
outside ([Internet](https://gitlab.com/gitlab-org/gitlab-foss/uploads/c4cc8cd353604bd80315f9384035ff9e/The_Internet_IT_Crowd.png)).
-For this example, **Sentinel 1** will be configured in the same machine as the
+For this example, **Sentinel 1** is configured in the same machine as the
**Redis Primary**, **Sentinel 2** and **Sentinel 3** in the same machines as the
**Replica 1** and **Replica 2** respectively.
@@ -261,11 +261,11 @@ Here is a list and description of each **machine** and the assigned **IP**:
- `10.0.0.4`: GitLab application
Please note that after the initial configuration, if a failover is initiated
-by the Sentinel nodes, the Redis nodes will be reconfigured and the **Primary**
-will change permanently (including in `redis.conf`) from one node to the other,
+by the Sentinel nodes, the Redis nodes are reconfigured and the **Primary**
+changes permanently (including in `redis.conf`) from one node to the other,
until a new failover is initiated again.
-The same thing will happen with `sentinel.conf` that will be overridden after the
+The same thing happens with `sentinel.conf` that is overridden after the
initial execution, after any new sentinel node starts watching the **Primary**,
or a failover promotes a different **Primary** node.
diff --git a/doc/development/experiment_guide/experimentation.md b/doc/development/experiment_guide/experimentation.md
index 7135f8acd9b..d631630b77e 100644
--- a/doc/development/experiment_guide/experimentation.md
+++ b/doc/development/experiment_guide/experimentation.md
@@ -6,6 +6,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Create an A/B test with `Experimentation Module`
+NOTE:
+We recommend using [GLEX](gitlab_experiment.md) for new experiments.
+
## Implement the experiment
1. Add the experiment to the `Gitlab::Experimentation::EXPERIMENTS` hash in
diff --git a/doc/development/experiment_guide/index.md b/doc/development/experiment_guide/index.md
index e39ef48517f..2cfb9517a60 100644
--- a/doc/development/experiment_guide/index.md
+++ b/doc/development/experiment_guide/index.md
@@ -46,16 +46,10 @@ One is built into GitLab directly and has been around for a while (this is calle
to as `Gitlab::Experiment` -- GLEX for short.
Both approaches use [experiment](../feature_flags/index.md#experiment-type)
-feature flags, and there is currently no strong suggestion to use one over the other.
+feature flags. We recommend using GLEX rather than `Experimentation Module` for new experiments.
-| Feature | `Experimentation Module` | GLEX |
-| -------------------- |------------------------- | ---- |
-| Record user grouping | Yes | No |
-| Uses feature flags | Yes | Yes |
-| Multivariate (A/B/n) | No | Yes |
-
-- [Implementing an A/B experiment using `Experimentation Module`](experimentation.md)
- [Implementing an A/B/n experiment using GLEX](gitlab_experiment.md)
+- [Implementing an A/B experiment using `Experimentation Module`](experimentation.md)
Historical Context: `Experimentation Module` was built iteratively with the needs that
appeared while implementing Growth sub-department experiments, while GLEX was built
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 49c511c2b85..f47a5164d43 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -105,6 +105,12 @@ Default client accepts two parameters: `resolvers` and `config`.
- `baseUrl` allows us to pass a URL for GraphQL endpoint different from our main endpoint (for example, `${gon.relative_url_root}/api/graphql`)
- `assumeImmutableResults` (set to `false` by default) - this setting, when set to `true`, assumes that every single operation on updating Apollo Cache is immutable. It also sets `freezeResults` to `true`, so any attempt on mutating Apollo Cache throws a console warning in development environment. Please ensure you're following the immutability pattern on cache update operations before setting this option to `true`.
- `fetchPolicy` determines how you want your component to interact with the Apollo cache. Defaults to "cache-first".
+
+### Multiple client queries for the same object
+
+If you are make multiple queries to the same Apollo client object you might encounter the following error: "Store error: the application attempted to write an object with no provided ID but the store already contains an ID of SomeEntity". [This error only should occur when you have made a query with an ID field for a portion, then made another that returns what would be the same object, but is missing the ID field.](https://github.com/apollographql/apollo-client/issues/2510#issue-271829009)
+
+Please note this is being tracked in [this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/326101) and the documentation will be updated when this issue is resolved.
## GraphQL Queries
diff --git a/doc/development/testing_guide/review_apps.md b/doc/development/testing_guide/review_apps.md
index c4194be23a4..4fd0003795d 100644
--- a/doc/development/testing_guide/review_apps.md
+++ b/doc/development/testing_guide/review_apps.md
@@ -132,6 +132,9 @@ the QA smoke suite.
You can also manually start the `review-qa-all`: it runs the full QA suite.
+After the end-to-end test runs have finished, [Allure reports](https://github.com/allure-framework/allure2) are generated and published by
+the `allure-report-qa-smoke` and `allure-report-qa-all` jobs. A comment with links to the reports are added to the merge request.
+
## Performance Metrics
On every [pipeline](https://gitlab.com/gitlab-org/gitlab/pipelines/125315730) in the `qa` stage, the
diff --git a/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb b/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb
new file mode 100644
index 00000000000..bba1ca26b35
--- /dev/null
+++ b/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+# rubocop: disable Style/Documentation
+class Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl
+ # rubocop: disable Gitlab/NamespacedClass
+ class JiraTrackerData < ActiveRecord::Base
+ self.table_name = "jira_tracker_data"
+ self.inheritance_column = :_type_disabled
+
+ include ::Integrations::BaseDataFields
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+
+ enum deployment_type: { unknown: 0, server: 1, cloud: 2 }, _prefix: :deployment
+ end
+ # rubocop: enable Gitlab/NamespacedClass
+
+ # https://rubular.com/r/uwgK7k9KH23efa
+ JIRA_CLOUD_REGEX = %r{^https?://[A-Za-z0-9](?:[A-Za-z0-9\-]{0,61}[A-Za-z0-9])?\.atlassian\.net$}ix.freeze
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(start_id, end_id)
+ trackers_data = JiraTrackerData
+ .where(deployment_type: 'unknown')
+ .where(id: start_id..end_id)
+
+ cloud, server = trackers_data.partition { |tracker_data| tracker_data.url.match?(JIRA_CLOUD_REGEX) }
+
+ cloud_mappings = cloud.each_with_object({}) do |tracker_data, hash|
+ hash[tracker_data] = { deployment_type: 2 }
+ end
+
+ server_mapppings = server.each_with_object({}) do |tracker_data, hash|
+ hash[tracker_data] = { deployment_type: 1 }
+ end
+
+ mappings = cloud_mappings.merge(server_mapppings)
+
+ ::Gitlab::Database::BulkUpdate.execute(%i[deployment_type], mappings)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/lib/gitlab/ci/ansi2json/line.rb b/lib/gitlab/ci/ansi2json/line.rb
index b1dee0e1ecc..466706384c0 100644
--- a/lib/gitlab/ci/ansi2json/line.rb
+++ b/lib/gitlab/ci/ansi2json/line.rb
@@ -77,7 +77,7 @@ module Gitlab
end
def set_section_duration(duration)
- @section_duration = Time.at(duration.to_i).strftime('%M:%S')
+ @section_duration = Time.at(duration.to_i).utc.strftime('%M:%S')
end
def flush_current_segment!
diff --git a/lib/gitlab/content_security_policy/config_loader.rb b/lib/gitlab/content_security_policy/config_loader.rb
index ef6fda5a505..73c694b7c1d 100644
--- a/lib/gitlab/content_security_policy/config_loader.rb
+++ b/lib/gitlab/content_security_policy/config_loader.rb
@@ -18,11 +18,11 @@ module Gitlab
'font_src' => "'self'",
'form_action' => "'self' https: http:",
'frame_ancestors' => "'self'",
- 'frame_src' => "'self' https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com",
+ 'frame_src' => "'self' https://www.google.com/recaptcha/ https://www.recaptcha.net/ https://content.googleapis.com https://content-compute.googleapis.com https://content-cloudbilling.googleapis.com https://content-cloudresourcemanager.googleapis.com",
'img_src' => "'self' data: blob: http: https:",
'manifest_src' => "'self'",
'media_src' => "'self'",
- 'script_src' => "'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com",
+ 'script_src' => "'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com",
'style_src' => "'self' 'unsafe-inline'",
'worker_src' => "'self'",
'object_src' => "'none'",
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 8de70c04f99..a5eaf76c47b 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -28955,6 +28955,9 @@ msgstr ""
msgid "SecurityConfiguration|By default, all analyzers are applied in order to cover all languages across your project, and only run if the language is detected in the Merge Request."
msgstr ""
+msgid "SecurityConfiguration|Compliance"
+msgstr ""
+
msgid "SecurityConfiguration|Configuration guide"
msgstr ""
@@ -28994,6 +28997,9 @@ msgstr ""
msgid "SecurityConfiguration|Manage"
msgstr ""
+msgid "SecurityConfiguration|Manage scans"
+msgstr ""
+
msgid "SecurityConfiguration|More information"
msgstr ""
@@ -29009,12 +29015,18 @@ msgstr ""
msgid "SecurityConfiguration|Security Control"
msgstr ""
+msgid "SecurityConfiguration|Security testing"
+msgstr ""
+
msgid "SecurityConfiguration|Status"
msgstr ""
msgid "SecurityConfiguration|Testing & Compliance"
msgstr ""
+msgid "SecurityConfiguration|The status of the tools only applies to the default branch and is based on the %{linkStart}latest pipeline%{linkEnd}. Once you've enabled a scan for the default branch, any subsequent feature branch you create will include the scan."
+msgstr ""
+
msgid "SecurityConfiguration|Using custom settings. You won't receive automatic updates on this variable. %{anchorStart}Restore to default%{anchorEnd}"
msgstr ""
diff --git a/spec/finders/security/security_jobs_finder_spec.rb b/spec/finders/security/security_jobs_finder_spec.rb
index fa8253b96b5..3023c42341b 100644
--- a/spec/finders/security/security_jobs_finder_spec.rb
+++ b/spec/finders/security/security_jobs_finder_spec.rb
@@ -28,19 +28,19 @@ RSpec.describe Security::SecurityJobsFinder do
end
end
- context 'with combination of security jobs and license management jobs' do
+ context 'with combination of security jobs and license scanning jobs' do
let!(:sast_build) { create(:ci_build, :sast, pipeline: pipeline) }
let!(:container_scanning_build) { create(:ci_build, :container_scanning, pipeline: pipeline) }
let!(:dast_build) { create(:ci_build, :dast, pipeline: pipeline) }
let!(:secret_detection_build) { create(:ci_build, :secret_detection, pipeline: pipeline) }
- let!(:license_management_build) { create(:ci_build, :license_management, pipeline: pipeline) }
+ let!(:license_scanning_build) { create(:ci_build, :license_scanning, pipeline: pipeline) }
it 'returns only the security jobs' do
is_expected.to include(sast_build)
is_expected.to include(container_scanning_build)
is_expected.to include(dast_build)
is_expected.to include(secret_detection_build)
- is_expected.not_to include(license_management_build)
+ is_expected.not_to include(license_scanning_build)
end
end
end
diff --git a/spec/frontend/security_configuration/components/redesigned_app_spec.js b/spec/frontend/security_configuration/components/redesigned_app_spec.js
new file mode 100644
index 00000000000..1b11f372d8d
--- /dev/null
+++ b/spec/frontend/security_configuration/components/redesigned_app_spec.js
@@ -0,0 +1,109 @@
+import { GlTab, GlTabs } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import {
+ SAST_NAME,
+ SAST_SHORT_NAME,
+ SAST_DESCRIPTION,
+ SAST_HELP_PATH,
+ SAST_CONFIG_HELP_PATH,
+} from '~/security_configuration/components/constants';
+import FeatureCard from '~/security_configuration/components/feature_card.vue';
+import RedesignedSecurityConfigurationApp, {
+ i18n,
+} from '~/security_configuration/components/redesigned_app.vue';
+import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
+
+describe('NewApp component', () => {
+ let wrapper;
+
+ const createComponent = (propsData) => {
+ wrapper = extendedWrapper(
+ mount(RedesignedSecurityConfigurationApp, {
+ propsData,
+ }),
+ );
+ };
+
+ const findMainHeading = () => wrapper.find('h1');
+ const findSubHeading = () => wrapper.find('h2');
+ const findTab = () => wrapper.findComponent(GlTab);
+ const findTabs = () => wrapper.findAllComponents(GlTabs);
+ const findByTestId = (id) => wrapper.findByTestId(id);
+ const findFeatureCards = () => wrapper.findAllComponents(FeatureCard);
+
+ const securityFeaturesMock = [
+ {
+ name: SAST_NAME,
+ shortName: SAST_SHORT_NAME,
+ description: SAST_DESCRIPTION,
+ helpPath: SAST_HELP_PATH,
+ configurationHelpPath: SAST_CONFIG_HELP_PATH,
+ type: REPORT_TYPE_SAST,
+ available: true,
+ },
+ ];
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('basic structure', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ });
+ });
+
+ it('renders main-heading with correct text', () => {
+ const mainHeading = findMainHeading();
+ expect(mainHeading).toExist();
+ expect(mainHeading.text()).toContain('Security Configuration');
+ });
+
+ it('renders GlTab Component ', () => {
+ expect(findTab()).toExist();
+ });
+
+ it('renders right amount of tabs with correct title ', () => {
+ expect(findTabs().length).toEqual(1);
+ });
+
+ it('renders security-testing tab', () => {
+ expect(findByTestId('security-testing-tab')).toExist();
+ });
+
+ it('renders sub-heading with correct text', () => {
+ const subHeading = findSubHeading();
+ expect(subHeading).toExist();
+ expect(subHeading.text()).toContain(i18n.securityTesting);
+ });
+
+ it('renders right amount of feature cards for given props with correct props', () => {
+ const cards = findFeatureCards();
+ expect(cards.length).toEqual(1);
+ expect(cards.at(0).props()).toEqual({ feature: securityFeaturesMock[0] });
+ });
+
+ it('should not show latest pipeline link when latestPipelinePath is not defined', () => {
+ expect(findByTestId('latest-pipeline-info').exists()).toBe(false);
+ });
+ });
+
+ describe('when given latestPipelinePath props', () => {
+ beforeEach(() => {
+ createComponent({
+ augmentedSecurityFeatures: securityFeaturesMock,
+ latestPipelinePath: 'test/path',
+ });
+ });
+
+ it('should show latest pipeline info with correct link when latestPipelinePath is defined', () => {
+ expect(findByTestId('latest-pipeline-info').exists()).toBe(true);
+ expect(findByTestId('latest-pipeline-info').text()).toMatchInterpolatedText(
+ i18n.securityTestingDescription,
+ );
+ expect(findByTestId('latest-pipeline-info').find('a').attributes('href')).toBe('test/path');
+ });
+ });
+});
diff --git a/spec/frontend/security_configuration/utils_spec.js b/spec/frontend/security_configuration/utils_spec.js
new file mode 100644
index 00000000000..6ad167cadda
--- /dev/null
+++ b/spec/frontend/security_configuration/utils_spec.js
@@ -0,0 +1,81 @@
+import { augmentFeatures } from '~/security_configuration/utils';
+
+const mockSecurityFeatures = [
+ {
+ name: 'SAST',
+ type: 'SAST',
+ },
+];
+
+const mockComplianceFeatures = [
+ {
+ name: 'LICENSE_COMPLIANCE',
+ type: 'LICENSE_COMPLIANCE',
+ },
+];
+
+const mockFeaturesWithSecondary = [
+ {
+ name: 'DAST',
+ type: 'DAST',
+ secondary: {
+ type: 'DAST PROFILES',
+ name: 'DAST PROFILES',
+ },
+ },
+];
+
+const mockInvalidCustomFeature = [
+ {
+ foo: 'bar',
+ },
+];
+
+const mockValidCustomFeature = [
+ {
+ name: 'SAST',
+ type: 'SAST',
+ customfield: 'customvalue',
+ },
+];
+
+const expectedOutputDefault = {
+ augmentedSecurityFeatures: mockSecurityFeatures,
+ augmentedComplianceFeatures: mockComplianceFeatures,
+};
+
+const expectedOutputSecondary = {
+ augmentedSecurityFeatures: mockSecurityFeatures,
+ augmentedComplianceFeatures: mockFeaturesWithSecondary,
+};
+
+const expectedOutputCustomFeature = {
+ augmentedSecurityFeatures: mockValidCustomFeature,
+ augmentedComplianceFeatures: mockComplianceFeatures,
+};
+
+describe('returns an object with augmentedSecurityFeatures and augmentedComplianceFeatures when', () => {
+ it('given an empty array', () => {
+ expect(augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, [])).toEqual(
+ expectedOutputDefault,
+ );
+ });
+
+ it('given an invalid populated array', () => {
+ expect(
+ augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockInvalidCustomFeature),
+ ).toEqual(expectedOutputDefault);
+ });
+
+ it('features have secondary key', () => {
+ expect(augmentFeatures(mockSecurityFeatures, mockFeaturesWithSecondary, [])).toEqual(
+ expectedOutputSecondary,
+ );
+ });
+
+ it('given a valid populated array', () => {
+ expect(
+ augmentFeatures(mockSecurityFeatures, mockComplianceFeatures, mockValidCustomFeature),
+ ).toEqual(expectedOutputCustomFeature);
+ });
+});
diff --git a/spec/graphql/mutations/ci/runner/update_spec.rb b/spec/graphql/mutations/ci/runner/update_spec.rb
index 302b680494d..3db0d552a05 100644
--- a/spec/graphql/mutations/ci/runner/update_spec.rb
+++ b/spec/graphql/mutations/ci/runner/update_spec.rb
@@ -66,14 +66,16 @@ RSpec.describe Mutations::Ci::Runner::Update do
context 'with valid arguments' do
it 'updates runner with correct values' do
- expected_attributes = mutation_params.except(:id)
+ expected_attributes = mutation_params.except(:id, :tag_list)
subject
expect(subject[:errors]).to be_empty
expect(subject[:runner]).to be_an_instance_of(Ci::Runner)
expect(subject[:runner]).to have_attributes(expected_attributes)
+ expect(subject[:runner].tag_list).to contain_exactly(*mutation_params[:tag_list])
expect(runner.reload).to have_attributes(expected_attributes)
+ expect(runner.tag_list).to contain_exactly(*mutation_params[:tag_list])
end
end
diff --git a/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb
new file mode 100644
index 00000000000..f7466a2ddfd
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/update_jira_tracker_data_deployment_type_based_on_url_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::BackgroundMigration::UpdateJiraTrackerDataDeploymentTypeBasedOnUrl do
+ let(:services_table) { table(:services) }
+ let(:service_jira_cloud) { services_table.create!(id: 1, type: 'JiraService') }
+ let(:service_jira_server) { services_table.create!(id: 2, type: 'JiraService') }
+
+ before do
+ jira_tracker_data = Class.new(ApplicationRecord) do
+ self.table_name = 'jira_tracker_data'
+
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :username, encryption_options
+ attr_encrypted :password, encryption_options
+ end
+
+ stub_const('JiraTrackerData', jira_tracker_data)
+ end
+
+ let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: 0) }
+ let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: 0) }
+
+ subject { described_class.new.perform(tracker_data_cloud.id, tracker_data_server.id) }
+
+ it "changes unknown deployment_types based on URL" do
+ expect(JiraTrackerData.pluck(:deployment_type)).to eq([0, 0])
+
+ subject
+
+ expect(JiraTrackerData.pluck(:deployment_type)).to eq([2, 1])
+ end
+end
diff --git a/spec/lib/gitlab/ci/ansi2json/line_spec.rb b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
index d681447a0e8..909c0f1b3ea 100644
--- a/spec/lib/gitlab/ci/ansi2json/line_spec.rb
+++ b/spec/lib/gitlab/ci/ansi2json/line_spec.rb
@@ -76,10 +76,30 @@ RSpec.describe Gitlab::Ci::Ansi2json::Line do
end
describe '#set_section_duration' do
- it 'sets and formats the section_duration' do
- subject.set_section_duration(75)
+ shared_examples 'set_section_duration' do
+ it 'sets and formats the section_duration' do
+ subject.set_section_duration(75)
- expect(subject.section_duration).to eq('01:15')
+ expect(subject.section_duration).to eq('01:15')
+ end
+ end
+
+ context 'with default timezone' do
+ it_behaves_like 'set_section_duration'
+ end
+
+ context 'with a timezone carrying minutes offset' do
+ before do
+ # The actual call by does use Time.at(...).utc that the following
+ # rubocop rule (Rails/TimeZone) suggests, but for this specific
+ # test's purposes we needed to mock at the Time.at call point.
+
+ # rubocop:disable Rails/TimeZone
+ allow(Time).to receive(:at).with(75).and_return(Time.at(75, in: '+05:30'))
+ # rubocop:enable Rails/TimeZone
+ end
+
+ it_behaves_like 'set_section_duration'
end
end
diff --git a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
index 3ae780e16f5..f2d77c3fd6d 100644
--- a/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
+++ b/spec/lib/gitlab/content_security_policy/config_loader_spec.rb
@@ -47,7 +47,7 @@ RSpec.describe Gitlab::ContentSecurityPolicy::ConfigLoader do
settings = described_class.default_settings_hash
directives = settings['directives']
- expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.recaptcha.net https://apis.google.com https://example.com")
+ expect(directives['script_src']).to eq("'strict-dynamic' 'self' 'unsafe-inline' 'unsafe-eval' https://www.google.com/recaptcha/ https://www.recaptcha.net https://apis.google.com https://example.com")
expect(directives['style_src']).to eq("'self' 'unsafe-inline' https://example.com")
end
end
diff --git a/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb b/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
new file mode 100644
index 00000000000..ae2e23ab539
--- /dev/null
+++ b/spec/migrations/20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url_spec.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20210421163509_schedule_update_jira_tracker_data_deployment_type_based_on_url.rb')
+
+RSpec.describe ScheduleUpdateJiraTrackerDataDeploymentTypeBasedOnUrl, :migration do
+ let(:services_table) { table(:services) }
+ let(:service_jira_cloud) { services_table.create!(id: 1, type: 'JiraService') }
+ let(:service_jira_server) { services_table.create!(id: 2, type: 'JiraService') }
+
+ before do
+ jira_tracker_data = Class.new(ApplicationRecord) do
+ self.table_name = 'jira_tracker_data'
+
+ def self.encryption_options
+ {
+ key: Settings.attr_encrypted_db_key_base_32,
+ encode: true,
+ mode: :per_attribute_iv,
+ algorithm: 'aes-256-gcm'
+ }
+ end
+
+ attr_encrypted :url, encryption_options
+ attr_encrypted :api_url, encryption_options
+ attr_encrypted :username, encryption_options
+ attr_encrypted :password, encryption_options
+ end
+
+ stub_const('JiraTrackerData', jira_tracker_data)
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+ end
+
+ let!(:tracker_data_cloud) { JiraTrackerData.create!(id: 1, service_id: service_jira_cloud.id, url: "https://test-domain.atlassian.net", deployment_type: 0) }
+ let!(:tracker_data_server) { JiraTrackerData.create!(id: 2, service_id: service_jira_server.id, url: "http://totally-not-jira-server.company.org", deployment_type: 0) }
+
+ around do |example|
+ freeze_time { Sidekiq::Testing.fake! { example.run } }
+ end
+
+ it 'schedules background migration' do
+ migrate!
+
+ expect(BackgroundMigrationWorker.jobs.size).to eq(2)
+ expect(described_class::MIGRATION).to be_scheduled_migration(tracker_data_cloud.id, tracker_data_cloud.id)
+ expect(described_class::MIGRATION).to be_scheduled_migration(tracker_data_server.id, tracker_data_server.id)
+ end
+end
diff --git a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
index 832d5afd957..925e7f96eee 100644
--- a/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
+++ b/spec/workers/authorized_project_update/user_refresh_over_user_range_worker_spec.rb
@@ -3,7 +3,8 @@
require 'spec_helper'
RSpec.describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker do
- let(:project) { create(:project) }
+ let_it_be(:project) { create(:project) }
+
let(:user) { project.namespace.owner }
let(:start_user_id) { user.id }
let(:end_user_id) { start_user_id }
@@ -64,6 +65,18 @@ RSpec.describe AuthorizedProjectUpdate::UserRefreshOverUserRangeWorker do
execute_worker
end
+
+ context 'when load balancing is enabled' do
+ before do
+ allow(Gitlab::Database::LoadBalancing).to receive(:enable?).and_return(true)
+ end
+
+ it 'reads from the primary database' do
+ expect(Gitlab::Database::LoadBalancing::Session.current).to receive(:use_primary!)
+
+ execute_worker
+ end
+ end
end
end
end