+
+
+
+ {{ content }}
+
+
+ {{ content }}
+
+
+
+
-
+
{{ s__('ErrorTracking|Active') }}
diff --git a/app/assets/javascripts/error_tracking_settings/constants.js b/app/assets/javascripts/error_tracking_settings/constants.js
new file mode 100644
index 00000000000..ee86c55e843
--- /dev/null
+++ b/app/assets/javascripts/error_tracking_settings/constants.js
@@ -0,0 +1,7 @@
+import { s__ } from '~/locale';
+
+export const I18N_ERROR_TRACKING_SETTINGS = {
+ integratedErrorTrackingDisabledText: s__(
+ 'ErrorTracking|Integrated error tracking is %{epicLinkStart}turned off by default%{epicLinkEnd} and no longer active for this project. To re-enable error tracking on self-hosted instances, you can either %{flagLinkStart}turn on the feature flag%{flagLinkEnd} for integrated error tracking, or provide a Sentry API URL and Auth Token below. However, error tracking is not ready for production use and cannot be enabled on GitLab.com.',
+ ),
+};
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 37a59d7f39d..0b9024dc3db 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -4,17 +4,6 @@ module MembershipActions
include MembersPresentation
extend ActiveSupport::Concern
- def create
- create_params = params.permit(:user_ids, :access_level, :expires_at)
- result = Members::CreateService.new(current_user, create_params.merge({ source: membershipable, invite_source: "#{plain_source_type}-members-page" })).execute
-
- if result[:status] == :success
- redirect_to members_page_url, notice: _('Users were successfully added.')
- else
- redirect_to members_page_url, alert: result[:message]
- end
- end
-
def update
update_params = params.require(root_params_key).permit(:access_level, :expires_at)
member = membershipable.members_and_requesters.find(params[:id])
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 1500cb1a8f0..ece1083d4d1 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -16,7 +16,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
before_action :authorize_admin_group_member!, except: admin_not_required_endpoints
skip_before_action :check_two_factor_requirement, only: :leave
- skip_cross_project_access_check :index, :create, :update, :destroy, :request_access,
+ skip_cross_project_access_check :index, :update, :destroy, :request_access,
:approve_access_request, :leave, :resend_invite,
:override
diff --git a/app/controllers/projects/error_tracking_controller.rb b/app/controllers/projects/error_tracking_controller.rb
index 8700d3c2198..9db56cb32b9 100644
--- a/app/controllers/projects/error_tracking_controller.rb
+++ b/app/controllers/projects/error_tracking_controller.rb
@@ -6,6 +6,10 @@ class Projects::ErrorTrackingController < Projects::ErrorTracking::BaseControlle
before_action :authorize_read_sentry_issue!
before_action :set_issue_id, only: :details
+ before_action only: [:index] do
+ push_frontend_feature_flag(:integrated_error_tracking, project)
+ end
+
def index
respond_to do |format|
format.html
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 56e201c592f..43c72b358db 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -7,6 +7,10 @@ module Projects
before_action :authorize_admin_operations!
before_action :authorize_read_prometheus_alerts!, only: [:reset_alerting_token]
+ before_action do
+ push_frontend_feature_flag(:integrated_error_tracking, project)
+ end
+
respond_to :json, only: [:reset_alerting_token, :reset_pagerduty_token]
helper_method :error_tracking_setting
diff --git a/app/helpers/projects/error_tracking_helper.rb b/app/helpers/projects/error_tracking_helper.rb
index 5be4f67bde8..471565d162c 100644
--- a/app/helpers/projects/error_tracking_helper.rb
+++ b/app/helpers/projects/error_tracking_helper.rb
@@ -12,7 +12,8 @@ module Projects::ErrorTrackingHelper
'error-tracking-enabled' => error_tracking_enabled.to_s,
'project-path' => project.full_path,
'list-path' => project_error_tracking_index_path(project),
- 'illustration-path' => image_path('illustrations/cluster_popover.svg')
+ 'illustration-path' => image_path('illustrations/cluster_popover.svg'),
+ 'show-integrated-tracking-disabled-alert' => show_integrated_tracking_disabled_alert?(project).to_s
}
end
@@ -27,4 +28,15 @@ module Projects::ErrorTrackingHelper
'issue-stack-trace-path' => stack_trace_project_error_tracking_index_path(*opts)
}
end
+
+ private
+
+ def show_integrated_tracking_disabled_alert?(project)
+ return false if ::Feature.enabled?(:integrated_error_tracking, project)
+
+ setting ||= project.error_tracking_setting ||
+ project.build_error_tracking_setting
+
+ setting.integrated_enabled?
+ end
end
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index 25f812645b1..0a429bb7afd 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -59,6 +59,10 @@ module ErrorTracking
integrated
end
+ def integrated_enabled?
+ enabled? && integrated_client?
+ end
+
def gitlab_dsn
strong_memoize(:gitlab_dsn) do
client_key&.sentry_dsn
diff --git a/app/models/integration.rb b/app/models/integration.rb
index 3cf71f46420..0445894644e 100644
--- a/app/models/integration.rb
+++ b/app/models/integration.rb
@@ -216,10 +216,6 @@ class Integration < ApplicationRecord
self.supported_events.map { |event| IntegrationsHelper.integration_event_field_name(event) }
end
- def self.supported_event_actions
- %w[]
- end
-
def self.supported_events
%w[commit push tag_push issue confidential_issue merge_request wiki_page]
end
@@ -501,10 +497,6 @@ class Integration < ApplicationRecord
end
end
- def configurable_event_actions
- self.class.supported_event_actions
- end
-
def supported_events
self.class.supported_events
end
diff --git a/app/models/integrations/jira.rb b/app/models/integrations/jira.rb
index 7308c731bb0..d8883be6820 100644
--- a/app/models/integrations/jira.rb
+++ b/app/models/integrations/jira.rb
@@ -80,10 +80,6 @@ module Integrations
%w(commit merge_request)
end
- def self.supported_event_actions
- %w(comment)
- end
-
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
def self.reference_pattern(only_long: true)
@reference_pattern ||= /(?\b#{Gitlab::Regex.jira_issue_key_regex})/
diff --git a/app/models/integrations/zentao.rb b/app/models/integrations/zentao.rb
index 493d42cc40b..0d437d335e9 100644
--- a/app/models/integrations/zentao.rb
+++ b/app/models/integrations/zentao.rb
@@ -48,10 +48,6 @@ module Integrations
%w()
end
- def self.supported_event_actions
- %w()
- end
-
def fields
[
{
diff --git a/config/feature_flags/development/integrated_error_tracking.yml b/config/feature_flags/development/integrated_error_tracking.yml
new file mode 100644
index 00000000000..fb302daed57
--- /dev/null
+++ b/config/feature_flags/development/integrated_error_tracking.yml
@@ -0,0 +1,8 @@
+---
+name: integrated_error_tracking
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/81767
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/353956
+milestone: '14.9'
+type: development
+group: group::respond
+default_enabled: false
diff --git a/config/routes/group.rb b/config/routes/group.rb
index c313f7209fb..41a165ab6e6 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -94,7 +94,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
concerns :clusterable
- resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+ resources :group_members, only: [:index, :update, :destroy], concerns: :access_requestable do
post :resend_invite, on: :member
delete :leave, on: :collection
end
diff --git a/config/routes/project.rb b/config/routes/project.rb
index f684ce97540..8cb7a86f02f 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -163,7 +163,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
- resources :project_members, except: [:show, :new, :edit], constraints: { id: %r{[a-zA-Z./0-9_\-#%+:]+} }, concerns: :access_requestable do
+ resources :project_members, except: [:show, :new, :create, :edit], constraints: { id: %r{[a-zA-Z./0-9_\-#%+:]+} }, concerns: :access_requestable do
collection do
delete :leave
diff --git a/db/post_migrate/20220305223212_add_security_training_providers.rb b/db/post_migrate/20220305223212_add_security_training_providers.rb
new file mode 100644
index 00000000000..fbddee0ae99
--- /dev/null
+++ b/db/post_migrate/20220305223212_add_security_training_providers.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+class AddSecurityTrainingProviders < Gitlab::Database::Migration[1.0]
+ KONTRA_DATA = {
+ name: 'Kontra',
+ description: "Kontra Application Security provides interactive developer security education that
+ enables engineers to quickly learn security best practices
+ and fix issues in their code by analysing real-world software security vulnerabilities.",
+ url: "https://application.security/api/webhook/gitlab/exercises/search"
+ }
+
+ SCW_DATA = {
+ name: 'Secure Code Warrior',
+ description: "Resolve vulnerabilities faster and confidently with highly relevant and bite-sized secure coding learning.",
+ url: "https://integration-api.securecodewarrior.com/api/v1/trial"
+ }
+
+ module Security
+ class TrainingProvider < ActiveRecord::Base
+ self.table_name = 'security_training_providers'
+ end
+ end
+
+ def up
+ current_time = Time.current
+ timestamps = { created_at: current_time, updated_at: current_time }
+
+ Security::TrainingProvider.reset_column_information
+
+ # upsert providers
+ Security::TrainingProvider.upsert_all([KONTRA_DATA.merge(timestamps), SCW_DATA.merge(timestamps)])
+ end
+
+ def down
+ Security::TrainingProvider.reset_column_information
+
+ Security::TrainingProvider.find_by(name: KONTRA_DATA[:name])&.destroy
+ Security::TrainingProvider.find_by(name: SCW_DATA[:name])&.destroy
+ end
+end
diff --git a/db/schema_migrations/20220305223212 b/db/schema_migrations/20220305223212
new file mode 100644
index 00000000000..b8adc88a760
--- /dev/null
+++ b/db/schema_migrations/20220305223212
@@ -0,0 +1 @@
+8a0e80b6df1d942e5ec23641c935103cddd96c044e2a960b88bb38284cf4d070
\ No newline at end of file
diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md
index 9e98d0d8069..6ffe145b54b 100644
--- a/doc/api/graphql/reference/index.md
+++ b/doc/api/graphql/reference/index.md
@@ -14416,6 +14416,10 @@ four standard [pagination arguments](#connection-pagination-arguments):
Network Policies of the project.
+WARNING:
+**Deprecated** in 14.8.
+Network policies are deprecated and will be removed in GitLab 15.0.
+
Returns [`NetworkPolicyConnection`](#networkpolicyconnection).
This field returns a [connection](#connections). It accepts the
diff --git a/doc/development/contributing/style_guides.md b/doc/development/contributing/style_guides.md
index da926005466..7a4ebbdbadf 100644
--- a/doc/development/contributing/style_guides.md
+++ b/doc/development/contributing/style_guides.md
@@ -159,25 +159,22 @@ When the number of RuboCop exceptions exceed the default [`exclude-limit` of 15]
we may want to resolve exceptions over multiple commits. To minimize confusion,
we should track our progress through the exception list.
-When auto-generating the `.rubocop_todo.yml` exception list for a particular Cop,
-and more than 15 files are affected, we should add the exception list to
-a different file in the directory `.rubocop_todo/`. For example, the configuration for the cop
-`Gitlab/NamespacedClass` is in `.rubocop_todo/gitlab/namespaced_class.yml`.
-
-This ensures that our list isn't mistakenly removed by another auto generation of
-the `.rubocop_todo.yml`. This also allows us greater visibility into the exceptions
-which are currently being resolved.
-
-One way to generate the initial list is to run the Rake task `rubocop:todo:generate`:
+The preferred way to [generate the initial list or a list for specific RuboCop rules](../rake_tasks.md#generate-initial-rubocop-todo-list)
+is to run the Rake task `rubocop:todo:generate`:
```shell
+# Initial list
bundle exec rake rubocop:todo:generate
+
+# List for specific RuboCop rules
+bundle exec rake 'rubocop:todo:generate[Gitlab/NamespacedClass,Lint/Syntax]'
```
-You can then move the list from the freshly generated `.rubocop_todo.yml` for the Cop being actively
-resolved and place it in the directory `.rubocop_todo/`. In this scenario, do not commit
-auto-generated changes to the `.rubocop_todo.yml`, as an `exclude limit` that is higher than 15
-makes the `.rubocop_todo.yml` hard to parse.
+This Rake task creates or updates the exception list in `.rubocop_todo/`. For
+example, the configuration for the RuboCop rule `Gitlab/NamespacedClass` is
+located in `.rubocop_todo/gitlab/namespaced_class.yml`.
+
+Make sure to commit any changes in `.rubocop_todo/` after running the Rake task.
### Reveal existing RuboCop exceptions
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index b98de46a72a..1e9367ecee4 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -196,6 +196,16 @@ One way to generate the initial list is to run the Rake task `rubocop:todo:gener
bundle exec rake rubocop:todo:generate
```
+To generate TODO list for specific RuboCop rules, pass them comma-seperated as
+argument to the Rake task:
+
+```shell
+bundle exec rake 'rubocop:todo:generate[Gitlab/NamespacedClass,Lint/Syntax]'
+bundle exec rake rubocop:todo:generate\[Gitlab/NamespacedClass,Lint/Syntax\]
+```
+
+Some shells require brackets to be escaped or quoted.
+
See [Resolving RuboCop exceptions](contributing/style_guides.md#resolving-rubocop-exceptions)
on how to proceed from here.
diff --git a/doc/operations/error_tracking.md b/doc/operations/error_tracking.md
index 738581fd040..907c59adacb 100644
--- a/doc/operations/error_tracking.md
+++ b/doc/operations/error_tracking.md
@@ -129,7 +129,17 @@ If another event occurs, the error reverts to unresolved.
## Integrated error tracking
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329596) in GitLab 14.4.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/329596) in GitLab 14.4.
+> - [Disabled](https://gitlab.com/gitlab-org/gitlab/-/issues/353639) in GitLab 14.9 [with a flag](../administration/feature_flags.md) named `integrated_error_tracking`. Disabled by default.
+
+FLAG:
+By default this feature is not available. To make it available on self-managed GitLab, ask an
+administrator to [enable the feature flag](../administration/feature_flags.md)
+named `integrated_error_tracking`. The feature is not ready for production use.
+On GitLab.com, this feature is not available.
+
+WARNING:
+Turning on integrated error tracking may impact performance, depending on your error rates.
Integrated error tracking is a lightweight alternative to Sentry backend.
You still use Sentry SDK with your application. But you don't need to deploy Sentry
diff --git a/doc/user/application_security/vulnerabilities/index.md b/doc/user/application_security/vulnerabilities/index.md
index fd96ac303a0..0b27760b4bb 100644
--- a/doc/user/application_security/vulnerabilities/index.md
+++ b/doc/user/application_security/vulnerabilities/index.md
@@ -27,10 +27,9 @@ On the vulnerability's page, you can:
- [Change the vulnerability's status](#change-vulnerability-status).
- [Create an issue](#create-an-issue-for-a-vulnerability).
- [Link issues to the vulnerability](#linked-issues).
-- [Resolve a vulnerability](#resolve-a-vulnerability), if a solution is
- available.
-
-In GitLab 14.9 and later, if security training is enabled, the vulnerability page includes a training link relevant to the detected vulnerability.
+- [Resolve a vulnerability](#resolve-a-vulnerability) if a solution is
+ available.
+- [View security training specific to the detected vulnerability](#view-security-training-for-a-vulnerability).
## Vulnerability status values
@@ -177,9 +176,13 @@ To enable security training for vulnerabilities in your project:
## View security training for a vulnerability
+> [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/6176) in GitLab 14.9.
+
+If security training is enabled, the vulnerability page includes a training link relevant to the detected vulnerability.
+
To view the security training for a vulnerability:
1. On the top bar, select **Menu > Projects** and find your project.
1. On the left sidebar, select **Security & Compliance > Vulnerability report**.
1. Select the vulnerability for which you want to view security training.
-1. If the security training provider supports training for the vulnerability, select **View training**.
+1. Select **View training**.
diff --git a/lib/api/entities/error_tracking.rb b/lib/api/entities/error_tracking.rb
index b55cba05ea0..163bda92680 100644
--- a/lib/api/entities/error_tracking.rb
+++ b/lib/api/entities/error_tracking.rb
@@ -9,6 +9,12 @@ module API
expose :sentry_external_url
expose :api_url
expose :integrated
+
+ def integrated
+ return false unless ::Feature.enabled?(:integrated_error_tracking, object.project)
+
+ object.integrated_client?
+ end
end
class ClientKey < Grape::Entity
diff --git a/lib/api/error_tracking/collector.rb b/lib/api/error_tracking/collector.rb
index 13fda356257..22a4e04a91c 100644
--- a/lib/api/error_tracking/collector.rb
+++ b/lib/api/error_tracking/collector.rb
@@ -28,8 +28,8 @@ module API
end
def feature_enabled?
- project.error_tracking_setting&.enabled? &&
- project.error_tracking_setting&.integrated_client?
+ Feature.enabled?(:integrated_error_tracking, project) &&
+ project.error_tracking_setting&.integrated_enabled?
end
def find_client_key(public_key)
diff --git a/lib/gitlab/health_checks/db_check.rb b/lib/gitlab/health_checks/db_check.rb
index ec4b97eaca4..3df312af1bc 100644
--- a/lib/gitlab/health_checks/db_check.rb
+++ b/lib/gitlab/health_checks/db_check.rb
@@ -13,12 +13,14 @@ module Gitlab
end
def successful?(result)
- result == '1'
+ result == Gitlab::Database.database_base_models.size
end
def check
catch_timeout 10.seconds do
- ActiveRecord::Base.connection.execute('SELECT 1 as ping')&.first&.[]('ping')&.to_s
+ Gitlab::Database.database_base_models.sum do |_, base|
+ base.connection.select_value('SELECT 1')
+ end
end
end
end
diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake
index 8c5edb5de8a..6eabdf51dcd 100644
--- a/lib/tasks/rubocop.rake
+++ b/lib/tasks/rubocop.rake
@@ -1,4 +1,5 @@
# frozen_string_literal: true
+# rubocop:disable Rails/RakeEnvironment
unless Rails.env.production?
require 'rubocop/rake_task'
@@ -8,18 +9,59 @@ unless Rails.env.production?
namespace :rubocop do
namespace :todo do
desc 'Generate RuboCop todos'
- task :generate do # rubocop:disable Rails/RakeEnvironment
+ task :generate do |_task, args|
require 'rubocop'
+ require 'active_support/inflector/inflections'
+ require_relative '../../rubocop/todo_dir'
+ require_relative '../../rubocop/formatter/todo_formatter'
+
+ # Reveal all pending TODOs so RuboCop can pick them up and report
+ # during scan.
+ ENV['REVEAL_RUBOCOP_TODO'] = '1'
+
+ # Save cop configuration like `RSpec/ContextWording` into
+ # `rspec/context_wording.yml` and not into
+ # `r_spec/context_wording.yml`.
+ ActiveSupport::Inflector.inflections(:en) do |inflect|
+ inflect.acronym 'RSpec'
+ inflect.acronym 'GraphQL'
+ end
options = %w[
- --auto-gen-config
- --auto-gen-only-exclude
- --exclude-limit=100000
- --no-offense-counts
+ --parallel
+ --format RuboCop::Formatter::TodoFormatter
]
+ # Convert from Rake::TaskArguments into an Array to make `any?` work as
+ # expected.
+ cop_names = args.to_a
+
+ todo_dir = RuboCop::TodoDir.new(RuboCop::TodoDir::DEFAULT_TODO_DIR)
+
+ if cop_names.any?
+ # We are sorting the cop names to benefit from RuboCop cache which
+ # also takes passed parameters into account.
+ list = cop_names.sort.join(',')
+ options.concat ['--only', list]
+
+ cop_names.each { |cop_name| todo_dir.inspect(cop_name) }
+ else
+ todo_dir.inspect_all
+ end
+
+ puts <<~MSG
+ Generating RuboCop TODOs with:
+ rubocop #{options.join(' ')}
+
+ This might take a while...
+ MSG
+
RuboCop::CLI.new.run(options)
+
+ todo_dir.delete_inspected
end
end
end
end
+
+# rubocop:enable Rails/RakeEnvironment
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1b40c3d4baa..62a0e006092 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -14443,6 +14443,12 @@ msgstr ""
msgid "ErrorTracking|If you self-host Sentry, enter your Sentry instance's full URL. If you use Sentry's hosted solution, enter https://sentry.io"
msgstr ""
+msgid "ErrorTracking|Integrated error tracking is %{epicLinkStart}turned off by default%{epicLinkEnd} and no longer active for this project. To re-enable error tracking on self-hosted instances, you can either %{flagLinkStart}turn on the feature flag%{flagLinkEnd} for integrated error tracking, or provide a %{settingsLinkStart}Sentry API URL and Auth Token%{settingsLinkEnd} on your project settings page. However, error tracking is not ready for production use and cannot be enabled on GitLab.com."
+msgstr ""
+
+msgid "ErrorTracking|Integrated error tracking is %{epicLinkStart}turned off by default%{epicLinkEnd} and no longer active for this project. To re-enable error tracking on self-hosted instances, you can either %{flagLinkStart}turn on the feature flag%{flagLinkEnd} for integrated error tracking, or provide a Sentry API URL and Auth Token below. However, error tracking is not ready for production use and cannot be enabled on GitLab.com."
+msgstr ""
+
msgid "ErrorTracking|No projects available"
msgstr ""
@@ -14452,6 +14458,9 @@ msgstr ""
msgid "ErrorTracking|To enable project selection, enter a valid Auth Token."
msgstr ""
+msgid "ErrorTracking|View project settings"
+msgstr ""
+
msgid "Errors"
msgstr ""
diff --git a/qa/qa/page/trials/new.rb b/qa/qa/page/trials/new.rb
index cd3b145a89e..40f593a7aa7 100644
--- a/qa/qa/page/trials/new.rb
+++ b/qa/qa/page/trials/new.rb
@@ -12,7 +12,7 @@ module QA
select :number_of_employees
text_field :telephone_number
select :country
- select :state, id: 'state'
+ select :state
button :continue
end
end
diff --git a/qa/qa/page/trials/select.rb b/qa/qa/page/trials/select.rb
index 3da0fb46322..39ef604a781 100644
--- a/qa/qa/page/trials/select.rb
+++ b/qa/qa/page/trials/select.rb
@@ -6,12 +6,11 @@ module QA
class Select < Chemlab::Page
path '/-/trials/select'
- # TODO: Supplant with data-qa-selectors
- select :subscription_for, id: 'namespace_id'
- text_field :new_group_name, id: 'new_group_name'
- button :start_your_free_trial, value: 'Start your free trial'
- radio :trial_company, id: 'trial_entity_company'
- radio :trial_individual, id: 'trial_entity_individual'
+ select :subscription_for
+ text_field :new_group_name
+ button :start_your_free_trial
+ radio :trial_company
+ radio :trial_individual
end
end
end
diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb
index d60b90b534f..0e6dd626312 100644
--- a/qa/qa/resource/fork.rb
+++ b/qa/qa/resource/fork.rb
@@ -95,7 +95,7 @@ module QA
def wait_until_forked
Runtime::Logger.debug("Waiting for the fork process to complete...")
forked = wait_until do
- project.import_status == "finished"
+ project.reload!.import_status == "finished"
end
raise "Timed out while waiting for the fork process to complete." unless forked
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
index e69702a8ffa..9c5c9992442 100644
--- a/qa/qa/resource/runner.rb
+++ b/qa/qa/resource/runner.rb
@@ -47,9 +47,8 @@ module QA
def remove_via_api!
runners = project.runners(tag_list: @tags)
- unless runners && !runners.empty?
- raise "Project #{project.path_with_namespace} has no runners#{" with tags #{@tags}." if @tags&.any?}"
- end
+
+ return if runners.blank?
this_runner = runners.find { |runner| runner[:description] == name }
unless this_runner
diff --git a/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb b/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb
new file mode 100644
index 00000000000..e69074e17e2
--- /dev/null
+++ b/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Verify', :runner do
+ describe 'Runner removal' do
+ include Support::API
+
+ let(:api_client) { Runtime::API::Client.new(:gitlab) }
+ let(:executor) { "qa-runner-#{Time.now.to_i}" }
+ let(:runner_tags) { ['runner-registration-e2e-test'] }
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = executor
+ runner.tags = runner_tags
+ end
+ end
+
+ before do
+ sleep 5 # Runner should register within 5 seconds
+ end
+
+ # Removing a runner via the UI is covered by `spec/features/runners_spec.rb``
+ it 'removes the runner' do
+ expect(runner.project.runners.size).to eq(1)
+ expect(runner.project.runners.first[:description]).to eq(executor)
+
+ request = Runtime::API::Request.new(api_client, "runners/#{runner.project.runners.first[:id]}")
+ response = delete(request.url)
+ expect(response.code).to eq(Support::API::HTTP_STATUS_NO_CONTENT)
+ expect(response.body).to be_empty
+
+ expect(runner.project.runners).to be_empty
+ end
+ end
+ end
+end
diff --git a/rubocop/formatter/todo_formatter.rb b/rubocop/formatter/todo_formatter.rb
new file mode 100644
index 00000000000..7c0911f5eba
--- /dev/null
+++ b/rubocop/formatter/todo_formatter.rb
@@ -0,0 +1,138 @@
+# frozen_string_literal: true
+
+require 'set'
+require 'rubocop'
+require 'yaml'
+
+require_relative '../todo_dir'
+
+module RuboCop
+ module Formatter
+ # This formatter dumps a YAML configuration file per cop rule
+ # into `.rubocop_todo/**/*.yml` which contains detected offenses.
+ #
+ # For example, this formatter stores offenses for `RSpec/VariableName`
+ # in `.rubocop_todo/rspec/variable_name.yml`.
+ class TodoFormatter < BaseFormatter
+ # Disable a cop which exceeds this limit. This way we ensure that we
+ # don't enable a cop by accident when moving it from
+ # .rubocop_todo.yml to .rubocop_todo/.
+ # We keep the cop disabled if it has been disabled previously explicitly
+ # via `Enabled: false` in .rubocop_todo.yml or .rubocop_todo/.
+ MAX_OFFENSE_COUNT = 15
+
+ Todo = Struct.new(:cop_name, :files, :offense_count) do
+ def initialize(cop_name)
+ super(cop_name, Set.new, 0)
+
+ @cop_class = RuboCop::Cop::Registry.global.find_by_cop_name(cop_name)
+ end
+
+ def record(file, offense_count)
+ files << file
+ self.offense_count += offense_count
+ end
+
+ def autocorrectable?
+ @cop_class&.support_autocorrect?
+ end
+ end
+
+ def initialize(output, options = {})
+ directory = options.delete(:rubocop_todo_dir) || TodoDir::DEFAULT_TODO_DIR
+ @todos = Hash.new { |hash, cop_name| hash[cop_name] = Todo.new(cop_name) }
+ @todo_dir = TodoDir.new(directory)
+ @config_inspect_todo_dir = load_config_inspect_todo_dir(directory)
+ @config_old_todo_yml = load_config_old_todo_yml(directory)
+ check_multiple_configurations!
+
+ super
+ end
+
+ def file_finished(file, offenses)
+ return if offenses.empty?
+
+ file = relative_path(file)
+
+ offenses.map(&:cop_name).tally.each do |cop_name, offense_count|
+ @todos[cop_name].record(file, offense_count)
+ end
+ end
+
+ def finished(_inspected_files)
+ @todos.values.sort_by(&:cop_name).each do |todo|
+ yaml = to_yaml(todo)
+ path = @todo_dir.write(todo.cop_name, yaml)
+
+ output.puts "Written to #{relative_path(path)}\n"
+ end
+ end
+
+ private
+
+ def relative_path(path)
+ parent = File.expand_path('..', @todo_dir.directory)
+ path.delete_prefix("#{parent}/")
+ end
+
+ def to_yaml(todo)
+ yaml = []
+ yaml << '---'
+ yaml << '# Cop supports --auto-correct.' if todo.autocorrectable?
+ yaml << "#{todo.cop_name}:"
+
+ if previously_disabled?(todo) && offense_count_exceeded?(todo)
+ yaml << " # Offense count: #{todo.offense_count}"
+ yaml << ' # Temporarily disabled due to too many offenses'
+ yaml << ' Enabled: false'
+ end
+
+ yaml << ' Exclude:'
+
+ files = todo.files.sort.map { |file| " - '#{file}'" }
+ yaml.concat files
+ yaml << ''
+
+ yaml.join("\n")
+ end
+
+ def offense_count_exceeded?(todo)
+ todo.offense_count > MAX_OFFENSE_COUNT
+ end
+
+ def check_multiple_configurations!
+ cop_names = @config_inspect_todo_dir.keys & @config_old_todo_yml.keys
+ return if cop_names.empty?
+
+ list = cop_names.sort.map { |cop_name| "- #{cop_name}" }.join("\n")
+ raise "Multiple configurations found for cops:\n#{list}\n"
+ end
+
+ def previously_disabled?(todo)
+ cop_name = todo.cop_name
+
+ config = @config_old_todo_yml[cop_name] ||
+ @config_inspect_todo_dir[cop_name] || {}
+ return false if config.empty?
+
+ config['Enabled'] == false
+ end
+
+ def load_config_inspect_todo_dir(directory)
+ @todo_dir.list_inspect.each_with_object({}) do |path, combined|
+ config = YAML.load_file(path)
+ combined.update(config) if Hash === config
+ end
+ end
+
+ # Load YAML configuration from `.rubocop_todo.yml`.
+ # We consider this file already old, obsolete, and to be removed soon.
+ def load_config_old_todo_yml(directory)
+ path = File.expand_path(File.join(directory, '../.rubocop_todo.yml'))
+ config = YAML.load_file(path) if File.exist?(path)
+
+ config || {}
+ end
+ end
+ end
+end
diff --git a/rubocop/todo_dir.rb b/rubocop/todo_dir.rb
new file mode 100644
index 00000000000..4aca4454a06
--- /dev/null
+++ b/rubocop/todo_dir.rb
@@ -0,0 +1,81 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+require 'active_support/inflector/inflections'
+
+module RuboCop
+ # Helper class to manage file access to RuboCop TODOs in .rubocop_todo directory.
+ class TodoDir
+ DEFAULT_TODO_DIR = File.expand_path('../.rubocop_todo', __dir__)
+
+ # Suffix to indicate TODOs being inspected right now.
+ SUFFIX_INSPECT = '.inspect'
+
+ attr_reader :directory
+
+ def initialize(directory, inflector: ActiveSupport::Inflector)
+ @directory = directory
+ @inflector = inflector
+ end
+
+ def read(cop_name, suffix = nil)
+ read_suffixed(cop_name)
+ end
+
+ def write(cop_name, content)
+ path = path_for(cop_name)
+
+ FileUtils.mkdir_p(File.dirname(path))
+ File.write(path, content)
+
+ path
+ end
+
+ def inspect(cop_name)
+ path = path_for(cop_name)
+
+ if File.exist?(path)
+ FileUtils.mv(path, "#{path}#{SUFFIX_INSPECT}")
+ true
+ else
+ false
+ end
+ end
+
+ def inspect_all
+ pattern = File.join(@directory, '**/*.yml')
+
+ Dir.glob(pattern).count do |path|
+ FileUtils.mv(path, "#{path}#{SUFFIX_INSPECT}")
+ end
+ end
+
+ def list_inspect
+ pattern = File.join(@directory, "**/*.yml.inspect")
+
+ Dir.glob(pattern)
+ end
+
+ def delete_inspected
+ pattern = File.join(@directory, '**/*.yml.inspect')
+
+ Dir.glob(pattern).count do |path|
+ File.delete(path)
+ end
+ end
+
+ private
+
+ def read_suffixed(cop_name, suffix = nil)
+ path = path_for(cop_name, suffix)
+
+ File.read(path) if File.exist?(path)
+ end
+
+ def path_for(cop_name, suffix = nil)
+ todo_path = "#{@inflector.underscore(cop_name)}.yml#{suffix}"
+
+ File.join(@directory, todo_path)
+ end
+ end
+end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 7e3448d764f..25d32436d58 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -99,107 +99,6 @@ RSpec.describe Groups::GroupMembersController do
end
end
- describe 'POST create' do
- let_it_be(:group_user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- context 'when user does not have enough rights' do
- before do
- group.add_developer(user)
- end
-
- it 'returns 403', :aggregate_failures do
- post :create, params: {
- group_id: group,
- user_ids: group_user.id,
- access_level: Gitlab::Access::GUEST
- }
-
- expect(response).to have_gitlab_http_status(:forbidden)
- expect(group.users).not_to include group_user
- end
- end
-
- context 'when user has enough rights' do
- before do
- group.add_owner(user)
- end
-
- it 'adds user to members', :aggregate_failures, :snowplow do
- post :create, params: {
- group_id: group,
- user_ids: group_user.id,
- access_level: Gitlab::Access::GUEST
- }
-
- expect(controller).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(group_group_members_path(group))
- expect(group.users).to include group_user
- expect_snowplow_event(
- category: 'Members::CreateService',
- action: 'create_member',
- label: 'group-members-page',
- property: 'existing_user',
- user: user
- )
- end
-
- it 'adds no user to members', :aggregate_failures do
- post :create, params: {
- group_id: group,
- user_ids: '',
- access_level: Gitlab::Access::GUEST
- }
-
- expect(controller).to set_flash.to 'No users specified.'
- expect(response).to redirect_to(group_group_members_path(group))
- expect(group.users).not_to include group_user
- end
- end
-
- context 'access expiry date' do
- before do
- group.add_owner(user)
- end
-
- subject do
- post :create, params: {
- group_id: group,
- user_ids: group_user.id,
- access_level: Gitlab::Access::GUEST,
- expires_at: expires_at
- }
- end
-
- context 'when set to a date in the past' do
- let(:expires_at) { 2.days.ago }
-
- it 'does not add user to members', :aggregate_failures do
- subject
-
- expect(flash[:alert]).to include('Expires at cannot be a date in the past')
- expect(response).to redirect_to(group_group_members_path(group))
- expect(group.users).not_to include group_user
- end
- end
-
- context 'when set to a date in the future' do
- let(:expires_at) { 5.days.from_now }
-
- it 'adds user to members', :aggregate_failures do
- subject
-
- expect(controller).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(group_group_members_path(group))
- expect(group.users).to include group_user
- end
- end
- end
- end
-
describe 'PUT update' do
let_it_be(:requester) { create(:group_member, :access_request, group: group) }
@@ -508,14 +407,6 @@ RSpec.describe Groups::GroupMembersController do
end
end
- describe 'POST #create' do
- it 'is successful' do
- post :create, params: { group_id: group, users: user, access_level: Gitlab::Access::GUEST }
-
- expect(response).to have_gitlab_http_status(:found)
- end
- end
-
describe 'PUT #update' do
it 'is successful' do
put :update,
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index d6af5976743..724c151c274 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -147,137 +147,6 @@ RSpec.describe Projects::ProjectMembersController do
end
end
- describe 'POST create' do
- let_it_be(:project_user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- context 'when user does not have enough rights' do
- before do
- project.add_developer(user)
- end
-
- it 'returns 404', :aggregate_failures do
- post :create, params: {
- namespace_id: project.namespace,
- project_id: project,
- user_ids: project_user.id,
- access_level: Gitlab::Access::GUEST
- }
-
- expect(response).to have_gitlab_http_status(:not_found)
- expect(project.users).not_to include project_user
- end
- end
-
- context 'when user has enough rights' do
- before do
- project.add_maintainer(user)
- end
-
- it 'adds user to members', :aggregate_failures, :snowplow do
- post :create, params: {
- namespace_id: project.namespace,
- project_id: project,
- user_ids: project_user.id,
- access_level: Gitlab::Access::GUEST
- }
-
- expect(controller).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(project_project_members_path(project))
- expect(project.users).to include project_user
- expect_snowplow_event(
- category: 'Members::CreateService',
- action: 'create_member',
- label: 'project-members-page',
- property: 'existing_user',
- user: user
- )
- end
-
- it 'adds no user to members', :aggregate_failures do
- expect_next_instance_of(Members::CreateService) do |instance|
- expect(instance).to receive(:execute).and_return(status: :failure, message: 'Message')
- end
-
- post :create, params: {
- namespace_id: project.namespace,
- project_id: project,
- user_ids: '',
- access_level: Gitlab::Access::GUEST
- }
-
- expect(controller).to set_flash.to 'Message'
- expect(response).to redirect_to(project_project_members_path(project))
- end
- end
-
- context 'adding project bot' do
- let_it_be(:project_bot) { create(:user, :project_bot) }
-
- before do
- project.add_maintainer(user)
-
- unrelated_project = create(:project)
- unrelated_project.add_maintainer(project_bot)
- end
-
- it 'returns error', :aggregate_failures do
- post :create, params: {
- namespace_id: project.namespace,
- project_id: project,
- user_ids: project_bot.id,
- access_level: Gitlab::Access::GUEST
- }
-
- expect(flash[:alert]).to include('project bots cannot be added to other groups / projects')
- expect(response).to redirect_to(project_project_members_path(project))
- end
- end
-
- context 'access expiry date' do
- before do
- project.add_maintainer(user)
- end
-
- subject do
- post :create, params: {
- namespace_id: project.namespace,
- project_id: project,
- user_ids: project_user.id,
- access_level: Gitlab::Access::GUEST,
- expires_at: expires_at
- }
- end
-
- context 'when set to a date in the past' do
- let(:expires_at) { 2.days.ago }
-
- it 'does not add user to members', :aggregate_failures do
- subject
-
- expect(flash[:alert]).to include('Expires at cannot be a date in the past')
- expect(response).to redirect_to(project_project_members_path(project))
- expect(project.users).not_to include project_user
- end
- end
-
- context 'when set to a date in the future' do
- let(:expires_at) { 5.days.from_now }
-
- it 'adds user to members', :aggregate_failures do
- subject
-
- expect(controller).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(project_project_members_path(project))
- expect(project.users).to include project_user
- end
- end
- end
- end
-
describe 'PUT update' do
let_it_be(:requester) { create(:project_member, :access_request, project: project) }
@@ -656,48 +525,6 @@ RSpec.describe Projects::ProjectMembersController do
end
end
- describe 'POST create' do
- let_it_be(:stranger) { create(:user) }
-
- context 'when creating owner' do
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'creates a member' do
- expect do
- post :create, params: {
- user_ids: stranger.id,
- namespace_id: project.namespace,
- access_level: Member::OWNER,
- project_id: project
- }
- end.to change { project.members.count }.by(1)
-
- expect(project.team_members).to include(user)
- end
- end
-
- context 'when create maintainer' do
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- it 'creates a member' do
- expect do
- post :create, params: {
- user_ids: stranger.id,
- namespace_id: project.namespace,
- access_level: Member::MAINTAINER,
- project_id: project
- }
- end.to change { project.members.count }.by(1)
- end
- end
- end
-
describe 'POST resend_invite' do
let_it_be(:member) { create(:project_member, project: project) }
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 03ae437a89e..4273da6c735 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -10,11 +10,7 @@ import {
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import {
- severityLevel,
- severityLevelVariant,
- errorStatus,
-} from '~/error_tracking/components/constants';
+import { severityLevel, severityLevelVariant, errorStatus } from '~/error_tracking/constants';
import ErrorDetails from '~/error_tracking/components/error_details.vue';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import {
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index 59671c175e7..acc94b25ade 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -7,6 +7,7 @@ import ErrorTrackingActions from '~/error_tracking/components/error_tracking_act
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/error_tracking/utils';
import Tracking from '~/tracking';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import errorsList from './list_mock.json';
Vue.use(Vuex);
@@ -25,28 +26,33 @@ describe('ErrorTrackingList', () => {
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPagination = () => wrapper.find(GlPagination);
const findErrorActions = () => wrapper.find(ErrorTrackingActions);
+ const findIntegratedDisabledAlert = () => wrapper.findByTestId('integrated-disabled-alert');
function mountComponent({
errorTrackingEnabled = true,
userCanEnableErrorTracking = true,
+ showIntegratedTrackingDisabledAlert = false,
stubs = {},
} = {}) {
- wrapper = mount(ErrorTrackingList, {
- store,
- propsData: {
- indexPath: '/path',
- listPath: '/error_tracking',
- projectPath: 'project/test',
- enableErrorTrackingLink: '/link',
- userCanEnableErrorTracking,
- errorTrackingEnabled,
- illustrationPath: 'illustration/path',
- },
- stubs: {
- ...stubChildren(ErrorTrackingList),
- ...stubs,
- },
- });
+ wrapper = extendedWrapper(
+ mount(ErrorTrackingList, {
+ store,
+ propsData: {
+ indexPath: '/path',
+ listPath: '/error_tracking',
+ projectPath: 'project/test',
+ enableErrorTrackingLink: '/link',
+ userCanEnableErrorTracking,
+ errorTrackingEnabled,
+ showIntegratedTrackingDisabledAlert,
+ illustrationPath: 'illustration/path',
+ },
+ stubs: {
+ ...stubChildren(ErrorTrackingList),
+ ...stubs,
+ },
+ }),
+ );
}
beforeEach(() => {
@@ -223,6 +229,31 @@ describe('ErrorTrackingList', () => {
});
});
+ describe('When the integrated tracking diabled alert should be shown', () => {
+ beforeEach(() => {
+ mountComponent({
+ showIntegratedTrackingDisabledAlert: true,
+ stubs: {
+ GlAlert: false,
+ },
+ });
+ });
+
+ it('shows the alert box', () => {
+ expect(findIntegratedDisabledAlert().exists()).toBe(true);
+ });
+
+ describe('when alert is dismissed', () => {
+ it('hides the alert box', async () => {
+ findIntegratedDisabledAlert().vm.$emit('dismiss');
+
+ await nextTick();
+
+ expect(findIntegratedDisabledAlert().exists()).toBe(false);
+ });
+ });
+ });
+
describe('When the ignore button on an error is clicked', () => {
beforeEach(() => {
store.state.list.loading = false;
diff --git a/spec/frontend/error_tracking_settings/components/app_spec.js b/spec/frontend/error_tracking_settings/components/app_spec.js
index 4d19ec047ef..4a0bbb1acbe 100644
--- a/spec/frontend/error_tracking_settings/components/app_spec.js
+++ b/spec/frontend/error_tracking_settings/components/app_spec.js
@@ -18,19 +18,27 @@ describe('error tracking settings app', () => {
let store;
let wrapper;
- function mountComponent() {
+ const defaultProps = {
+ initialEnabled: 'true',
+ initialIntegrated: 'false',
+ initialApiHost: TEST_HOST,
+ initialToken: 'someToken',
+ initialProject: null,
+ listProjectsEndpoint: TEST_HOST,
+ operationsSettingsEndpoint: TEST_HOST,
+ gitlabDsn: TEST_GITLAB_DSN,
+ };
+
+ function mountComponent({
+ glFeatures = { integratedErrorTracking: false },
+ props = defaultProps,
+ } = {}) {
wrapper = extendedWrapper(
shallowMount(ErrorTrackingSettings, {
store, // Override the imported store
- propsData: {
- initialEnabled: 'true',
- initialIntegrated: 'false',
- initialApiHost: TEST_HOST,
- initialToken: 'someToken',
- initialProject: null,
- listProjectsEndpoint: TEST_HOST,
- operationsSettingsEndpoint: TEST_HOST,
- gitlabDsn: TEST_GITLAB_DSN,
+ propsData: { ...props },
+ provide: {
+ glFeatures,
},
stubs: {
GlFormInputGroup, // we need this non-shallow to query for a component within a slot
@@ -47,6 +55,7 @@ describe('error tracking settings app', () => {
const findElementWithText = (wrappers, text) => wrappers.filter((item) => item.text() === text);
const findSentrySettings = () => wrapper.findByTestId('sentry-setting-form');
const findDsnSettings = () => wrapper.findByTestId('gitlab-dsn-setting-form');
+ const findEnabledCheckbox = () => wrapper.findByTestId('error-tracking-enabled');
const enableGitLabErrorTracking = async () => {
findBackendSettingsRadioGroup().vm.$emit('change', true);
@@ -88,62 +97,104 @@ describe('error tracking settings app', () => {
});
describe('tracking-backend settings', () => {
- it('contains a form-group with the correct label', () => {
- expect(findBackendSettingsSection().attributes('label')).toBe('Error tracking backend');
+ it('does not contain backend settings section', () => {
+ expect(findBackendSettingsSection().exists()).toBe(false);
});
- it('contains a radio group', () => {
- expect(findBackendSettingsRadioGroup().exists()).toBe(true);
- });
-
- it('contains the correct radio buttons', () => {
- expect(findBackendSettingsRadioButtons()).toHaveLength(2);
-
- expect(findElementWithText(findBackendSettingsRadioButtons(), 'Sentry')).toHaveLength(1);
- expect(findElementWithText(findBackendSettingsRadioButtons(), 'GitLab')).toHaveLength(1);
- });
-
- it('hides the Sentry settings when GitLab is selected as a tracking-backend', async () => {
+ it('shows the sentry form', () => {
expect(findSentrySettings().exists()).toBe(true);
-
- await enableGitLabErrorTracking();
-
- expect(findSentrySettings().exists()).toBe(false);
});
- describe('GitLab DSN section', () => {
- it('is visible when GitLab is selected as a tracking-backend and DSN is present', async () => {
- expect(findDsnSettings().exists()).toBe(false);
+ describe('enabled setting is true', () => {
+ describe('integrated setting is true', () => {
+ beforeEach(() => {
+ mountComponent({
+ props: { ...defaultProps, initialEnabled: 'true', initialIntegrated: 'true' },
+ });
+ });
- await enableGitLabErrorTracking();
-
- expect(findDsnSettings().exists()).toBe(true);
+ it('displays enabled as false', () => {
+ expect(findEnabledCheckbox().attributes('checked')).toBeUndefined();
+ });
});
- it('contains copy-to-clipboard functionality for the GitLab DSN string', async () => {
- await enableGitLabErrorTracking();
+ describe('integrated setting is false', () => {
+ beforeEach(() => {
+ mountComponent({
+ props: { ...defaultProps, initialEnabled: 'true', initialIntegrated: 'false' },
+ });
+ });
- const clipBoardInput = findDsnSettings().findComponent(GlFormInputGroup);
- const clipBoardButton = findDsnSettings().findComponent(ClipboardButton);
-
- expect(clipBoardInput.props('value')).toBe(TEST_GITLAB_DSN);
- expect(clipBoardInput.attributes('readonly')).toBeTruthy();
- expect(clipBoardButton.props('text')).toBe(TEST_GITLAB_DSN);
+ it('displays enabled as true', () => {
+ expect(findEnabledCheckbox().attributes('checked')).toBe('true');
+ });
});
});
- it.each([true, false])(
- 'calls the `updateIntegrated` action when the setting changes to `%s`',
- (integrated) => {
- jest.spyOn(store, 'dispatch').mockImplementation();
+ describe('integrated_error_tracking feature flag enabled', () => {
+ beforeEach(() => {
+ mountComponent({
+ glFeatures: { integratedErrorTracking: true },
+ });
+ });
- expect(store.dispatch).toHaveBeenCalledTimes(0);
+ it('contains a form-group with the correct label', () => {
+ expect(findBackendSettingsSection().attributes('label')).toBe('Error tracking backend');
+ });
- findBackendSettingsRadioGroup().vm.$emit('change', integrated);
+ it('contains a radio group', () => {
+ expect(findBackendSettingsRadioGroup().exists()).toBe(true);
+ });
- expect(store.dispatch).toHaveBeenCalledTimes(1);
- expect(store.dispatch).toHaveBeenCalledWith('updateIntegrated', integrated);
- },
- );
+ it('contains the correct radio buttons', () => {
+ expect(findBackendSettingsRadioButtons()).toHaveLength(2);
+
+ expect(findElementWithText(findBackendSettingsRadioButtons(), 'Sentry')).toHaveLength(1);
+ expect(findElementWithText(findBackendSettingsRadioButtons(), 'GitLab')).toHaveLength(1);
+ });
+
+ it('hides the Sentry settings when GitLab is selected as a tracking-backend', async () => {
+ expect(findSentrySettings().exists()).toBe(true);
+
+ await enableGitLabErrorTracking();
+
+ expect(findSentrySettings().exists()).toBe(false);
+ });
+
+ describe('GitLab DSN section', () => {
+ it('is visible when GitLab is selected as a tracking-backend and DSN is present', async () => {
+ expect(findDsnSettings().exists()).toBe(false);
+
+ await enableGitLabErrorTracking();
+
+ expect(findDsnSettings().exists()).toBe(true);
+ });
+
+ it('contains copy-to-clipboard functionality for the GitLab DSN string', async () => {
+ await enableGitLabErrorTracking();
+
+ const clipBoardInput = findDsnSettings().findComponent(GlFormInputGroup);
+ const clipBoardButton = findDsnSettings().findComponent(ClipboardButton);
+
+ expect(clipBoardInput.props('value')).toBe(TEST_GITLAB_DSN);
+ expect(clipBoardInput.attributes('readonly')).toBeTruthy();
+ expect(clipBoardButton.props('text')).toBe(TEST_GITLAB_DSN);
+ });
+ });
+
+ it.each([true, false])(
+ 'calls the `updateIntegrated` action when the setting changes to `%s`',
+ (integrated) => {
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ expect(store.dispatch).toHaveBeenCalledTimes(0);
+
+ findBackendSettingsRadioGroup().vm.$emit('change', integrated);
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ expect(store.dispatch).toHaveBeenCalledWith('updateIntegrated', integrated);
+ },
+ );
+ });
});
});
diff --git a/spec/helpers/projects/error_tracking_helper_spec.rb b/spec/helpers/projects/error_tracking_helper_spec.rb
index 882031a9c86..9f6b9241cbd 100644
--- a/spec/helpers/projects/error_tracking_helper_spec.rb
+++ b/spec/helpers/projects/error_tracking_helper_spec.rb
@@ -34,7 +34,8 @@ RSpec.describe Projects::ErrorTrackingHelper do
'error-tracking-enabled' => 'false',
'list-path' => list_path,
'project-path' => project_path,
- 'illustration-path' => match_asset_path('/assets/illustrations/cluster_popover.svg')
+ 'illustration-path' => match_asset_path('/assets/illustrations/cluster_popover.svg'),
+ 'show-integrated-tracking-disabled-alert' => 'false'
)
end
end
@@ -67,6 +68,37 @@ RSpec.describe Projects::ErrorTrackingHelper do
)
end
end
+
+ context 'with integrated error tracking feature' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:feature_flag, :enabled, :integrated, :show_alert) do
+ false | true | true | true
+ false | true | false | false
+ false | false | true | false
+ false | false | false | false
+ true | true | true | false
+ true | true | false | false
+ true | false | true | false
+ true | false | false | false
+ end
+
+ with_them do
+ before do
+ stub_feature_flags(integrated_error_tracking: feature_flag)
+ error_tracking_setting.update_columns(
+ enabled: enabled,
+ integrated: integrated
+ )
+ end
+
+ specify do
+ expect(helper.error_tracking_data(current_user, project)).to include(
+ 'show-integrated-tracking-disabled-alert' => show_alert.to_s
+ )
+ end
+ end
+ end
end
context 'when user is not maintainer' do
diff --git a/spec/lib/gitlab/health_checks/db_check_spec.rb b/spec/lib/gitlab/health_checks/db_check_spec.rb
index 60ebc596a0f..09b2650eae8 100644
--- a/spec/lib/gitlab/health_checks/db_check_spec.rb
+++ b/spec/lib/gitlab/health_checks/db_check_spec.rb
@@ -4,5 +4,20 @@ require 'spec_helper'
require_relative './simple_check_shared'
RSpec.describe Gitlab::HealthChecks::DbCheck do
- include_examples 'simple_check', 'db_ping', 'Db', '1'
+ include_examples 'simple_check', 'db_ping', 'Db', Gitlab::Database.database_base_models.size
+
+ context 'with multiple databases' do
+ subject { described_class.readiness }
+
+ before do
+ allow(Gitlab::Database).to receive(:database_base_models)
+ .and_return({ main: ApplicationRecord, ci: Ci::ApplicationRecord }.with_indifferent_access)
+ end
+
+ it 'checks multiple databases' do
+ expect(ApplicationRecord.connection).to receive(:select_value).with('SELECT 1').and_call_original
+ expect(Ci::ApplicationRecord.connection).to receive(:select_value).with('SELECT 1').and_call_original
+ expect(subject).to have_attributes(success: true)
+ end
+ end
end
diff --git a/spec/migrations/20220305223212_add_security_training_providers_spec.rb b/spec/migrations/20220305223212_add_security_training_providers_spec.rb
new file mode 100644
index 00000000000..3d0089aaa8d
--- /dev/null
+++ b/spec/migrations/20220305223212_add_security_training_providers_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require_migration!
+
+RSpec.describe AddSecurityTrainingProviders, :migration do
+ include MigrationHelpers::WorkItemTypesHelper
+
+ let_it_be(:security_training_providers) { table(:security_training_providers) }
+
+ it 'creates default data' do
+ # Need to delete all as security training providers are seeded before entire test suite
+ security_training_providers.delete_all
+
+ reversible_migration do |migration|
+ migration.before -> {
+ expect(security_training_providers.count).to eq(0)
+ }
+
+ migration.after -> {
+ expect(security_training_providers.count).to eq(2)
+ }
+ end
+ end
+end
diff --git a/spec/models/error_tracking/project_error_tracking_setting_spec.rb b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
index d17541b4a6c..d700eb5eaf7 100644
--- a/spec/models/error_tracking/project_error_tracking_setting_spec.rb
+++ b/spec/models/error_tracking/project_error_tracking_setting_spec.rb
@@ -535,6 +535,25 @@ RSpec.describe ErrorTracking::ProjectErrorTrackingSetting do
end
end
+ describe '#integrated_enabled?' do
+ using RSpec::Parameterized::TableSyntax
+
+ where(:enabled, :integrated, :integrated_enabled) do
+ true | false | false
+ false | true | false
+ true | true | true
+ end
+
+ with_them do
+ before do
+ subject.enabled = enabled
+ subject.integrated = integrated
+ end
+
+ it { expect(subject.integrated_enabled?).to eq(integrated_enabled) }
+ end
+ end
+
describe '#gitlab_dsn' do
let!(:client_key) { create(:error_tracking_client_key, project: project) }
diff --git a/spec/requests/api/error_tracking/collector_spec.rb b/spec/requests/api/error_tracking/collector_spec.rb
index 771bab20b75..fa0b238dcad 100644
--- a/spec/requests/api/error_tracking/collector_spec.rb
+++ b/spec/requests/api/error_tracking/collector_spec.rb
@@ -26,7 +26,6 @@ RSpec.describe API::ErrorTracking::Collector do
RSpec.shared_examples 'successful request' do
it 'writes to the database and returns OK', :aggregate_failures do
expect { subject }.to change { ErrorTracking::ErrorEvent.count }.by(1)
-
expect(response).to have_gitlab_http_status(:ok)
end
end
@@ -42,6 +41,14 @@ RSpec.describe API::ErrorTracking::Collector do
it_behaves_like 'successful request'
+ context 'intergrated error tracking feature flag is disabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: false)
+ end
+
+ it_behaves_like 'not found'
+ end
+
context 'error tracking feature is disabled' do
before do
setting.update!(enabled: false)
diff --git a/spec/requests/api/error_tracking/project_settings_spec.rb b/spec/requests/api/error_tracking/project_settings_spec.rb
index 161e4f01ea5..c0c0680ef31 100644
--- a/spec/requests/api/error_tracking/project_settings_spec.rb
+++ b/spec/requests/api/error_tracking/project_settings_spec.rb
@@ -23,6 +23,21 @@ RSpec.describe API::ErrorTracking::ProjectSettings do
end
end
+ shared_examples 'returns project settings with false for integrated' do
+ specify do
+ make_request
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(json_response).to eq(
+ 'active' => setting.reload.enabled,
+ 'project_name' => setting.project_name,
+ 'sentry_external_url' => setting.sentry_external_url,
+ 'api_url' => setting.api_url,
+ 'integrated' => false
+ )
+ end
+ end
+
shared_examples 'returns 404' do
it 'returns no project settings' do
make_request
@@ -46,7 +61,17 @@ RSpec.describe API::ErrorTracking::ProjectSettings do
end
context 'patch settings' do
- it_behaves_like 'returns project settings'
+ context 'integrated_error_tracking feature enabled' do
+ it_behaves_like 'returns project settings'
+ end
+
+ context 'integrated_error_tracking feature disabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: false)
+ end
+
+ it_behaves_like 'returns project settings with false for integrated'
+ end
it 'updates enabled flag' do
expect(setting).to be_enabled
@@ -84,13 +109,19 @@ RSpec.describe API::ErrorTracking::ProjectSettings do
context 'with integrated param' do
let(:params) { { active: true, integrated: true } }
- it 'updates the integrated flag' do
- expect(setting.integrated).to be_falsey
+ context 'integrated_error_tracking feature enabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: true)
+ end
- make_request
+ it 'updates the integrated flag' do
+ expect(setting.integrated).to be_falsey
- expect(json_response).to include('integrated' => true)
- expect(setting.reload.integrated).to be_truthy
+ make_request
+
+ expect(json_response).to include('integrated' => true)
+ expect(setting.reload.integrated).to be_truthy
+ end
end
end
end
@@ -170,7 +201,21 @@ RSpec.describe API::ErrorTracking::ProjectSettings do
end
context 'get settings' do
- it_behaves_like 'returns project settings'
+ context 'integrated_error_tracking feature enabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: true)
+ end
+
+ it_behaves_like 'returns project settings'
+ end
+
+ context 'integrated_error_tracking feature disabled' do
+ before do
+ stub_feature_flags(integrated_error_tracking: false)
+ end
+
+ it_behaves_like 'returns project settings with false for integrated'
+ end
end
end
diff --git a/spec/requests/api/invitations_spec.rb b/spec/requests/api/invitations_spec.rb
index d9d98815a7d..741cf793a77 100644
--- a/spec/requests/api/invitations_spec.rb
+++ b/spec/requests/api/invitations_spec.rb
@@ -208,6 +208,25 @@ RSpec.describe API::Invitations do
end
end
+ context 'when adding project bot' do
+ let_it_be(:project_bot) { create(:user, :project_bot) }
+
+ before do
+ unrelated_project = create(:project)
+ unrelated_project.add_maintainer(project_bot)
+ end
+
+ it 'returns error' do
+ expect do
+ post invitations_url(source, maintainer),
+ params: { email: project_bot.email, access_level: Member::DEVELOPER }
+
+ expect(json_response['status']).to eq 'error'
+ expect(json_response['message'][project_bot.email]).to include('User project bots cannot be added to other groups / projects')
+ end.not_to change { source.members.count }
+ end
+ end
+
it "returns a message if member already exists" do
post invitations_url(source, maintainer),
params: { email: developer.email, access_level: Member::MAINTAINER }
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index 425b758e748..79236b990d0 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -395,7 +395,7 @@ RSpec.describe 'project routing' do
# DELETE /:project_id/project_members/:id(.:format) project_members#destroy
describe Projects::ProjectMembersController, 'routing' do
it_behaves_like 'resource routing' do
- let(:actions) { %i[index create update destroy] }
+ let(:actions) { %i[index update destroy] }
let(:base_path) { '/gitlab/gitlabhq/-/project_members' }
end
end
diff --git a/spec/rubocop/formatter/todo_formatter_spec.rb b/spec/rubocop/formatter/todo_formatter_spec.rb
new file mode 100644
index 00000000000..e1b1de33bfe
--- /dev/null
+++ b/spec/rubocop/formatter/todo_formatter_spec.rb
@@ -0,0 +1,284 @@
+# frozen_string_literal: true
+# rubocop:disable RSpec/VerifiedDoubles
+
+require 'fast_spec_helper'
+require 'stringio'
+require 'fileutils'
+
+require_relative '../../../rubocop/formatter/todo_formatter'
+require_relative '../../../rubocop/todo_dir'
+
+RSpec.describe RuboCop::Formatter::TodoFormatter do
+ let(:stdout) { StringIO.new }
+ let(:tmp_dir) { Dir.mktmpdir }
+ let(:real_tmp_dir) { File.join(tmp_dir, 'real') }
+ let(:symlink_tmp_dir) { File.join(tmp_dir, 'symlink') }
+ let(:rubocop_todo_dir) { "#{symlink_tmp_dir}/.rubocop_todo" }
+ let(:options) { { rubocop_todo_dir: rubocop_todo_dir } }
+ let(:todo_dir) { RuboCop::TodoDir.new(rubocop_todo_dir) }
+
+ subject(:formatter) { described_class.new(stdout, options) }
+
+ around do |example|
+ FileUtils.mkdir(real_tmp_dir)
+ FileUtils.symlink(real_tmp_dir, symlink_tmp_dir)
+
+ Dir.chdir(symlink_tmp_dir) do
+ example.run
+ end
+ end
+
+ after do
+ FileUtils.remove_entry(tmp_dir)
+ end
+
+ context 'with offenses detected' do
+ let(:offense) { fake_offense('A/Offense') }
+ let(:offense_too_many) { fake_offense('B/TooManyOffenses') }
+ let(:offense_autocorrect) { fake_offense('B/AutoCorrect') }
+
+ before do
+ stub_const("#{described_class}::MAX_OFFENSE_COUNT", 1)
+
+ stub_rubocop_registry(
+ 'A/Offense' => { autocorrectable: false },
+ 'B/AutoCorrect' => { autocorrectable: true }
+ )
+ end
+
+ def run_formatter
+ formatter.started(%w[a.rb b.rb c.rb d.rb])
+ formatter.file_finished('c.rb', [offense_too_many])
+ formatter.file_finished('a.rb', [offense_too_many, offense, offense_too_many])
+ formatter.file_finished('b.rb', [])
+ formatter.file_finished('d.rb', [offense_autocorrect])
+ formatter.finished(%w[a.rb b.rb c.rb d.rb])
+ end
+
+ it 'outputs its actions' do
+ run_formatter
+
+ expect(stdout.string).to eq(<<~OUTPUT)
+ Written to .rubocop_todo/a/offense.yml
+ Written to .rubocop_todo/b/auto_correct.yml
+ Written to .rubocop_todo/b/too_many_offenses.yml
+ OUTPUT
+ end
+
+ it 'creates YAML files', :aggregate_failures do
+ run_formatter
+
+ expect(rubocop_todo_dir_listing).to contain_exactly(
+ 'a/offense.yml', 'b/auto_correct.yml', 'b/too_many_offenses.yml'
+ )
+
+ expect(todo_yml('A/Offense')).to eq(<<~YAML)
+ ---
+ A/Offense:
+ Exclude:
+ - 'a.rb'
+ YAML
+
+ expect(todo_yml('B/AutoCorrect')).to eq(<<~YAML)
+ ---
+ # Cop supports --auto-correct.
+ B/AutoCorrect:
+ Exclude:
+ - 'd.rb'
+ YAML
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+
+ context 'when cop previously not explicitly disabled' do
+ before do
+ todo_dir.write('B/TooManyOffenses', <<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ it 'does not disable cop' do
+ run_formatter
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
+ context 'when cop previously explicitly disabled in rubocop_todo/' do
+ before do
+ todo_dir.write('B/TooManyOffenses', <<~YAML)
+ ---
+ B/TooManyOffenses:
+ Enabled: false
+ Exclude:
+ - 'x.rb'
+ YAML
+
+ todo_dir.inspect_all
+ end
+
+ it 'keeps cop disabled' do
+ run_formatter
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ # Offense count: 3
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
+ context 'when cop previously explicitly disabled in rubocop_todo.yml' do
+ before do
+ File.write('.rubocop_todo.yml', <<~YAML)
+ ---
+ B/TooManyOffenses:
+ Enabled: false
+ Exclude:
+ - 'x.rb'
+ YAML
+ end
+
+ it 'keeps cop disabled' do
+ run_formatter
+
+ expect(todo_yml('B/TooManyOffenses')).to eq(<<~YAML)
+ ---
+ B/TooManyOffenses:
+ # Offense count: 3
+ # Temporarily disabled due to too many offenses
+ Enabled: false
+ Exclude:
+ - 'a.rb'
+ - 'c.rb'
+ YAML
+ end
+ end
+
+ context 'with cop configuration in both .rubocop_todo/ and .rubocop_todo.yml' do
+ before do
+ todo_dir.write('B/TooManyOffenses', <<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'a.rb'
+ YAML
+
+ todo_dir.write('A/Offense', <<~YAML)
+ ---
+ A/Offense:
+ Exclude:
+ - 'a.rb'
+ YAML
+
+ todo_dir.inspect_all
+
+ File.write('.rubocop_todo.yml', <<~YAML)
+ ---
+ B/TooManyOffenses:
+ Exclude:
+ - 'x.rb'
+ A/Offense:
+ Exclude:
+ - 'y.rb'
+ YAML
+ end
+
+ it 'raises an error' do
+ expect { run_formatter }.to raise_error(RuntimeError, <<~TXT)
+ Multiple configurations found for cops:
+ - A/Offense
+ - B/TooManyOffenses
+ TXT
+ end
+ end
+ end
+
+ context 'without offenses detected' do
+ before do
+ formatter.started(%w[a.rb b.rb])
+ formatter.file_finished('a.rb', [])
+ formatter.file_finished('b.rb', [])
+ formatter.finished(%w[a.rb b.rb])
+ end
+
+ it 'does not output anything' do
+ expect(stdout.string).to eq('')
+ end
+
+ it 'does not write any YAML files' do
+ expect(rubocop_todo_dir_listing).to be_empty
+ end
+ end
+
+ context 'without files to inspect' do
+ before do
+ formatter.started([])
+ formatter.finished([])
+ end
+
+ it 'does not output anything' do
+ expect(stdout.string).to eq('')
+ end
+
+ it 'does not write any YAML files' do
+ expect(rubocop_todo_dir_listing).to be_empty
+ end
+ end
+
+ private
+
+ def rubocop_todo_dir_listing
+ Dir.glob("#{rubocop_todo_dir}/**/*")
+ .select { |path| File.file?(path) }
+ .map { |path| path.delete_prefix("#{rubocop_todo_dir}/") }
+ end
+
+ def todo_yml(cop_name)
+ todo_dir.read(cop_name)
+ end
+
+ def fake_offense(cop_name)
+ double(:offense, cop_name: cop_name)
+ end
+
+ def stub_rubocop_registry(**cops)
+ rubocop_registry = double(:rubocop_registry)
+
+ allow(RuboCop::Cop::Registry).to receive(:global).and_return(rubocop_registry)
+
+ allow(rubocop_registry).to receive(:find_by_cop_name)
+ .with(String).and_return(nil)
+
+ cops.each do |cop_name, attributes|
+ allow(rubocop_registry).to receive(:find_by_cop_name)
+ .with(cop_name).and_return(fake_cop(**attributes))
+ end
+ end
+
+ def fake_cop(autocorrectable:)
+ double(:cop, support_autocorrect?: autocorrectable)
+ end
+end
+
+# rubocop:enable RSpec/VerifiedDoubles
diff --git a/spec/rubocop/todo_dir_spec.rb b/spec/rubocop/todo_dir_spec.rb
new file mode 100644
index 00000000000..ae59def885d
--- /dev/null
+++ b/spec/rubocop/todo_dir_spec.rb
@@ -0,0 +1,218 @@
+# frozen_string_literal: true
+
+require 'fast_spec_helper'
+require 'fileutils'
+require 'active_support/inflector/inflections'
+
+require_relative '../../rubocop/todo_dir'
+
+RSpec.describe RuboCop::TodoDir do
+ let(:todo_dir) { described_class.new(directory) }
+ let(:directory) { Dir.mktmpdir }
+ let(:cop_name) { 'RSpec/VariableInstance' }
+ let(:cop_name_underscore) { ActiveSupport::Inflector.underscore(cop_name) }
+ let(:yaml_path) { "#{File.join(directory, cop_name_underscore)}.yml" }
+
+ around do |example|
+ Dir.chdir(directory) do
+ example.run
+ end
+ end
+
+ after do
+ FileUtils.remove_entry(directory)
+ end
+
+ describe '#initialize' do
+ context 'when passing inflector' do
+ let(:fake_inflector) { double(:inflector) } # rubocop:disable RSpec/VerifiedDoubles
+ let(:todo_dir) { described_class.new(directory, inflector: fake_inflector) }
+
+ before do
+ allow(fake_inflector).to receive(:underscore)
+ .with(cop_name)
+ .and_return(cop_name_underscore)
+ end
+
+ it 'calls .underscore' do
+ todo_dir.write(cop_name, 'a')
+
+ expect(fake_inflector).to have_received(:underscore)
+ end
+ end
+ end
+
+ describe '#directory' do
+ subject { todo_dir.directory }
+
+ it { is_expected.to eq(directory) }
+ end
+
+ describe '#read' do
+ let(:content) { 'a' }
+
+ subject { todo_dir.read(cop_name) }
+
+ context 'when file exists' do
+ before do
+ todo_dir.write(cop_name, content)
+ end
+
+ it { is_expected.to eq(content) }
+ end
+
+ context 'when file is missing' do
+ it { is_expected.to be_nil }
+ end
+ end
+
+ describe '#write' do
+ let(:content) { 'a' }
+
+ subject { todo_dir.write(cop_name, content) }
+
+ it { is_expected.to eq(yaml_path) }
+
+ it 'writes content to YAML file' do
+ subject
+
+ expect(File.read(yaml_path)).to eq(content)
+ end
+ end
+
+ describe '#inspect' do
+ subject { todo_dir.inspect(cop_name) }
+
+ context 'with existing YAML file' do
+ before do
+ todo_dir.write(cop_name, 'a')
+ end
+
+ it { is_expected.to eq(true) }
+
+ it 'moves YAML file to .inspect' do
+ subject
+
+ expect(File).not_to exist(yaml_path)
+ expect(File).to exist("#{yaml_path}.inspect")
+ end
+ end
+
+ context 'with missing YAML file' do
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#inspect_all' do
+ subject { todo_dir.inspect_all }
+
+ context 'with YAML files' do
+ before do
+ todo_dir.write(cop_name, 'a')
+ todo_dir.write('Other/Rule', 'a')
+ todo_dir.write('Very/Nested/Rule', 'a')
+ end
+
+ it { is_expected.to eq(3) }
+
+ it 'moves all YAML files to .inspect' do
+ subject
+
+ expect(Dir.glob('**/*.yml')).to be_empty
+ expect(Dir.glob('**/*.yml.inspect').size).to eq(3)
+ end
+ end
+
+ context 'with non-YAML files' do
+ before do
+ File.write('file', 'a')
+ File.write('file.txt', 'a')
+ File.write('file.yaml', 'a') # not .yml
+ end
+
+ it { is_expected.to eq(0) }
+
+ it 'does not move non-YAML files' do
+ subject
+
+ expect(Dir.glob('**/*'))
+ .to contain_exactly('file', 'file.txt', 'file.yaml')
+ end
+ end
+
+ context 'without files' do
+ it { is_expected.to eq(0) }
+ end
+ end
+
+ describe '#list_inspect' do
+ let(:content) { 'a' }
+
+ subject { todo_dir.list_inspect }
+
+ context 'when file exists and is being inspected' do
+ before do
+ todo_dir.write(cop_name, content)
+ todo_dir.inspect_all
+ end
+
+ it do
+ is_expected.to contain_exactly("#{yaml_path}.inspect")
+ end
+ end
+
+ context 'when file exists but not being inspected' do
+ before do
+ todo_dir.write(cop_name, content)
+ end
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'when file is missing' do
+ it { is_expected.to be_empty }
+ end
+ end
+
+ describe '#delete_inspected' do
+ subject { todo_dir.delete_inspected }
+
+ context 'with YAML files' do
+ before do
+ todo_dir.write(cop_name, 'a')
+ todo_dir.write('Other/Rule', 'a')
+ todo_dir.write('Very/Nested/Rule', 'a')
+ todo_dir.inspect_all
+ end
+
+ it { is_expected.to eq(3) }
+
+ it 'deletes all .inspected YAML files' do
+ subject
+
+ expect(Dir.glob('**/*.yml.inspect')).to be_empty
+ end
+ end
+
+ context 'with non-YAML files' do
+ before do
+ File.write('file.inspect', 'a')
+ File.write('file.txt.inspect', 'a')
+ File.write('file.yaml.inspect', 'a') # not .yml
+ end
+
+ it { is_expected.to eq(0) }
+
+ it 'does not delete non-YAML files' do
+ subject
+
+ expect(Dir.glob('**/*')).to contain_exactly(
+ 'file.inspect', 'file.txt.inspect', 'file.yaml.inspect')
+ end
+ end
+
+ context 'without files' do
+ it { is_expected.to eq(0) }
+ end
+ end
+end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 724b8be36ad..a72c8d2c4e8 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -458,11 +458,6 @@ RSpec.configure do |config|
end
end
- # Allows stdout to be redirected to reduce noise
- config.before(:each, :silence_stdout) do
- $stdout = StringIO.new
- end
-
# Makes diffs show entire non-truncated values.
config.before(:each, unlimited_max_formatted_output_length: true) do |_example|
config.expect_with :rspec do |c|
@@ -475,10 +470,6 @@ RSpec.configure do |config|
allow_any_instance_of(VersionCheck).to receive(:response).and_return({ "severity" => "success" })
end
- config.after(:each, :silence_stdout) do
- $stdout = STDOUT
- end
-
config.disable_monkey_patching!
end
diff --git a/spec/support/silence_stdout.rb b/spec/support/silence_stdout.rb
new file mode 100644
index 00000000000..b2bc65c5cda
--- /dev/null
+++ b/spec/support/silence_stdout.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+RSpec.configure do |config|
+ # Allows stdout to be redirected to reduce noise
+ config.before(:each, :silence_stdout) do
+ $stdout = StringIO.new
+ end
+
+ config.after(:each, :silence_stdout) do
+ $stdout = STDOUT
+ end
+end
diff --git a/spec/tasks/rubocop_rake_spec.rb b/spec/tasks/rubocop_rake_spec.rb
new file mode 100644
index 00000000000..cf7e45aae28
--- /dev/null
+++ b/spec/tasks/rubocop_rake_spec.rb
@@ -0,0 +1,168 @@
+# frozen_string_literal: true
+# rubocop:disable RSpec/VerifiedDoubles
+
+require 'fast_spec_helper'
+require 'rake'
+require 'fileutils'
+
+require_relative '../support/silence_stdout'
+require_relative '../support/helpers/next_instance_of'
+require_relative '../support/helpers/rake_helpers'
+require_relative '../../rubocop/todo_dir'
+
+RSpec.describe 'rubocop rake tasks', :silence_stdout do
+ include RakeHelpers
+
+ before do
+ stub_const('Rails', double(:rails_env))
+ allow(Rails).to receive(:env).and_return(double(production?: false))
+
+ stub_const('ENV', ENV.to_hash.dup)
+
+ Rake.application.rake_require 'tasks/rubocop'
+ end
+
+ describe 'todo:generate', :aggregate_failures do
+ let(:tmp_dir) { Dir.mktmpdir }
+ let(:rubocop_todo_dir) { File.join(tmp_dir, '.rubocop_todo') }
+ let(:todo_dir) { RuboCop::TodoDir.new(rubocop_todo_dir) }
+
+ around do |example|
+ Dir.chdir(tmp_dir) do
+ with_inflections do
+ example.run
+ end
+ end
+ end
+
+ before do
+ allow(RuboCop::TodoDir).to receive(:new).and_return(todo_dir)
+
+ # This Ruby file will trigger the following 3 offenses.
+ File.write('a.rb', <<~RUBY)
+ a+b
+
+ RUBY
+
+ # Mimic GitLab's .rubocop_todo.yml avoids relying on RuboCop's
+ # default.yml configuration.
+ File.write('.rubocop.yml', <<~YAML)
+ <% unless ENV['REVEAL_RUBOCOP_TODO'] == '1' %>
+ <% Dir.glob('.rubocop_todo/**/*.yml').each do |rubocop_todo_yaml| %>
+ - '<%= rubocop_todo_yaml %>'
+ <% end %>
+ - '.rubocop_todo.yml'
+ <% end %>
+
+ AllCops:
+ NewCops: enable # Avoiding RuboCop warnings
+
+ Layout/SpaceAroundOperators:
+ Enabled: true
+
+ Layout/TrailingEmptyLines:
+ Enabled: true
+
+ Lint/Syntax:
+ Enabled: true
+
+ Style/FrozenStringLiteralComment:
+ Enabled: true
+ YAML
+
+ # Required to verify that we are revealing all TODOs via
+ # ENV['REVEAL_RUBOCOP_TODO'] = '1'.
+ # This file can be removed from specs after we've moved all offenses from
+ # .rubocop_todo.yml to .rubocop_todo/**/*.yml.
+ File.write('.rubocop_todo.yml', <<~YAML)
+ # Too many offenses
+ Layout/SpaceAroundOperators:
+ Enabled: false
+ YAML
+
+ # Previous offense now fixed.
+ todo_dir.write('Lint/Syntax', '')
+ end
+
+ after do
+ FileUtils.remove_entry(tmp_dir)
+ end
+
+ context 'without arguments' do
+ let(:run_task) { run_rake_task('rubocop:todo:generate') }
+
+ it 'generates TODOs for all RuboCop rules' do
+ expect { run_task }.to output(<<~OUTPUT).to_stdout
+ Generating RuboCop TODOs with:
+ rubocop --parallel --format RuboCop::Formatter::TodoFormatter
+
+ This might take a while...
+ Written to .rubocop_todo/layout/space_around_operators.yml
+ Written to .rubocop_todo/layout/trailing_empty_lines.yml
+ Written to .rubocop_todo/style/frozen_string_literal_comment.yml
+ OUTPUT
+
+ expect(rubocop_todo_dir_listing).to contain_exactly(
+ 'layout/space_around_operators.yml',
+ 'layout/trailing_empty_lines.yml',
+ 'style/frozen_string_literal_comment.yml'
+ )
+ end
+
+ it 'sets acronyms for inflections' do
+ run_task
+
+ expect(ActiveSupport::Inflector.inflections.acronyms).to include(
+ 'rspec' => 'RSpec',
+ 'graphql' => 'GraphQL'
+ )
+ end
+ end
+
+ context 'with cop names as arguments' do
+ let(:run_task) do
+ cop_names = %w[
+ Style/FrozenStringLiteralComment Layout/TrailingEmptyLines
+ Lint/Syntax
+ ]
+
+ run_rake_task('rubocop:todo:generate', cop_names)
+ end
+
+ it 'generates TODOs for given RuboCop cops' do
+ expect { run_task }.to output(<<~OUTPUT).to_stdout
+ Generating RuboCop TODOs with:
+ rubocop --parallel --format RuboCop::Formatter::TodoFormatter --only Layout/TrailingEmptyLines,Lint/Syntax,Style/FrozenStringLiteralComment
+
+ This might take a while...
+ Written to .rubocop_todo/layout/trailing_empty_lines.yml
+ Written to .rubocop_todo/style/frozen_string_literal_comment.yml
+ OUTPUT
+
+ expect(rubocop_todo_dir_listing).to contain_exactly(
+ 'layout/trailing_empty_lines.yml',
+ 'style/frozen_string_literal_comment.yml'
+ )
+ end
+ end
+
+ private
+
+ def rubocop_todo_dir_listing
+ Dir.glob("#{rubocop_todo_dir}/**/*")
+ .select { |path| File.file?(path) }
+ .map { |path| path.delete_prefix("#{rubocop_todo_dir}/") }
+ end
+
+ def with_inflections
+ original = ActiveSupport::Inflector::Inflections.instance_variable_get(:@__instance__)[:en]
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original.dup)
+
+ yield
+ ensure
+ ActiveSupport::Inflector::Inflections.instance_variable_set(:@__instance__, en: original)
+ end
+ end
+end
+
+# rubocop:enable RSpec/VerifiedDoubles