Add latest changes from gitlab-org/gitlab@master

This commit is contained in:
GitLab Bot 2022-03-10 00:08:36 +00:00
parent 1bcebb67d5
commit 237ead18b9
56 changed files with 1609 additions and 472 deletions

View File

@ -1,21 +0,0 @@
export const severityLevel = {
FATAL: 'fatal',
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
DEBUG: 'debug',
};
export const severityLevelVariant = {
[severityLevel.FATAL]: 'danger',
[severityLevel.ERROR]: 'neutral',
[severityLevel.WARNING]: 'warning',
[severityLevel.INFO]: 'info',
[severityLevel.DEBUG]: 'muted',
};
export const errorStatus = {
IGNORED: 'ignored',
RESOLVED: 'resolved',
UNRESOLVED: 'unresolved',
};

View File

@ -26,7 +26,7 @@ import {
trackErrorStatusUpdateOptions,
} from '../utils';
import { severityLevel, severityLevelVariant, errorStatus } from './constants';
import { severityLevel, severityLevelVariant, errorStatus } from '../constants';
import Stacktrace from './stacktrace.vue';
const SENTRY_TIMEOUT = 10000;

View File

@ -1,5 +1,6 @@
<script>
import {
GlAlert,
GlEmptyState,
GlButton,
GlIcon,
@ -10,6 +11,7 @@ import {
GlDropdown,
GlDropdownItem,
GlDropdownDivider,
GlSprintf,
GlTooltipDirective,
GlPagination,
} from '@gitlab/ui';
@ -21,6 +23,7 @@ import { __ } from '~/locale';
import Tracking from '~/tracking';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '../utils';
import { I18N_ERROR_TRACKING_LIST } from '../constants';
import ErrorTrackingActions from './error_tracking_actions.vue';
export const tableDataClass = 'table-col d-flex d-md-table-cell align-items-center';
@ -29,6 +32,7 @@ export default {
FIRST_PAGE: 1,
PREV_PAGE: 1,
NEXT_PAGE: 2,
i18n: I18N_ERROR_TRACKING_LIST,
fields: [
{
key: 'error',
@ -71,6 +75,7 @@ export default {
frequency: __('Frequency'),
},
components: {
GlAlert,
GlEmptyState,
GlButton,
GlDropdown,
@ -81,6 +86,7 @@ export default {
GlLoadingIcon,
GlTable,
GlFormInput,
GlSprintf,
GlPagination,
TimeAgo,
ErrorTrackingActions,
@ -117,12 +123,17 @@ export default {
type: String,
required: true,
},
showIntegratedTrackingDisabledAlert: {
type: Boolean,
required: false,
},
},
hasLocalStorage: AccessorUtils.canUseLocalStorage(),
data() {
return {
errorSearchQuery: '',
pageValue: this.$options.FIRST_PAGE,
isAlertDismissed: false,
};
},
computed: {
@ -142,6 +153,9 @@ export default {
errorTrackingHelpUrl() {
return helpPagePath('operations/error_tracking');
},
showIntegratedDisabledAlert() {
return !this.isAlertDismissed && this.showIntegratedTrackingDisabledAlert;
},
},
watch: {
pagination() {
@ -150,6 +164,8 @@ export default {
}
},
},
epicLink: 'https://gitlab.com/gitlab-org/gitlab/-/issues/353639',
featureFlagLink: helpPagePath('operations/error_tracking'),
created() {
if (this.errorTrackingEnabled) {
this.setEndpoint(this.indexPath);
@ -232,6 +248,34 @@ export default {
<template>
<div class="error-list">
<div v-if="errorTrackingEnabled">
<gl-alert
v-if="showIntegratedDisabledAlert"
variant="danger"
data-testid="integrated-disabled-alert"
@dismiss="isAlertDismissed = true"
>
<gl-sprintf :message="this.$options.i18n.integratedErrorTrackingDisabledText">
<template #epicLink="{ content }">
<gl-link :href="$options.epicLink" target="_blank">{{ content }}</gl-link>
</template>
<template #flagLink="{ content }">
<gl-link :href="$options.featureFlagLink" target="_blank">{{ content }}</gl-link>
</template>
<template #settingsLink="{ content }">
<gl-link :href="enableErrorTrackingLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
<div>
<gl-button
category="primary"
variant="confirm"
:href="enableErrorTrackingLink"
class="gl-mr-auto gl-mt-3"
>
{{ $options.i18n.viewProjectSettingsButton }}
</gl-button>
</div>
</gl-alert>
<div
class="row flex-column flex-md-row align-items-md-center m-0 mt-sm-2 p-3 p-sm-3 bg-secondary border"
>

View File

@ -0,0 +1,30 @@
import { s__ } from '~/locale';
export const severityLevel = {
FATAL: 'fatal',
ERROR: 'error',
WARNING: 'warning',
INFO: 'info',
DEBUG: 'debug',
};
export const severityLevelVariant = {
[severityLevel.FATAL]: 'danger',
[severityLevel.ERROR]: 'neutral',
[severityLevel.WARNING]: 'warning',
[severityLevel.INFO]: 'info',
[severityLevel.DEBUG]: 'muted',
};
export const errorStatus = {
IGNORED: 'ignored',
RESOLVED: 'resolved',
UNRESOLVED: 'unresolved',
};
export const I18N_ERROR_TRACKING_LIST = {
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 %{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.',
),
viewProjectSettingsButton: s__('ErrorTracking|View project settings'),
};

View File

@ -14,10 +14,15 @@ export default () => {
projectPath,
listPath,
} = domEl.dataset;
let { errorTrackingEnabled, userCanEnableErrorTracking } = domEl.dataset;
let {
errorTrackingEnabled,
userCanEnableErrorTracking,
showIntegratedTrackingDisabledAlert,
} = domEl.dataset;
errorTrackingEnabled = parseBoolean(errorTrackingEnabled);
userCanEnableErrorTracking = parseBoolean(userCanEnableErrorTracking);
showIntegratedTrackingDisabledAlert = parseBoolean(showIntegratedTrackingDisabledAlert);
// eslint-disable-next-line no-new
new Vue({
@ -36,6 +41,7 @@ export default () => {
userCanEnableErrorTracking,
projectPath,
listPath,
showIntegratedTrackingDisabledAlert,
},
});
},

View File

@ -1,29 +1,40 @@
<script>
import {
GlAlert,
GlButton,
GlFormGroup,
GlFormCheckbox,
GlFormRadioGroup,
GlFormRadio,
GlFormInputGroup,
GlLink,
GlSprintf,
} from '@gitlab/ui';
import { mapActions, mapGetters, mapState } from 'vuex';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import glFeatureFlagsMixin from '~/vue_shared/mixins/gl_feature_flags_mixin';
import { helpPagePath } from '~/helpers/help_page_helper';
import { I18N_ERROR_TRACKING_SETTINGS } from '../constants';
import ErrorTrackingForm from './error_tracking_form.vue';
import ProjectDropdown from './project_dropdown.vue';
export default {
i18n: I18N_ERROR_TRACKING_SETTINGS,
components: {
ErrorTrackingForm,
GlAlert,
GlButton,
GlFormCheckbox,
GlFormGroup,
GlFormRadioGroup,
GlFormRadio,
GlFormInputGroup,
GlLink,
GlSprintf,
ProjectDropdown,
ClipboardButton,
},
mixins: [glFeatureFlagsMixin()],
props: {
initialApiHost: {
type: String,
@ -62,6 +73,11 @@ export default {
default: null,
},
},
data() {
return {
isAlertDismissed: false,
};
},
computed: {
...mapGetters([
'dropdownLabel',
@ -81,12 +97,34 @@ export default {
showGitlabDsnSetting() {
return this.integrated && this.enabled && this.gitlabDsn;
},
showIntegratedErrorTracking() {
return this.glFeatures.integratedErrorTracking === true;
},
setInitialEnabled() {
if (this.showIntegratedErrorTracking) {
return this.initialEnabled;
}
if (this.initialIntegrated === 'true') {
return 'false';
}
return this.initialEnabled;
},
showIntegratedTrackingDisabledAlert() {
return (
!this.isAlertDismissed &&
!this.showIntegratedErrorTracking &&
this.initialIntegrated === 'true' &&
this.initialEnabled === 'true'
);
},
},
epicLink: 'https://gitlab.com/gitlab-org/gitlab/-/issues/353639',
featureFlagLink: helpPagePath('operations/error_tracking'),
created() {
this.setInitialState({
apiHost: this.initialApiHost,
enabled: this.initialEnabled,
integrated: this.initialIntegrated,
enabled: this.setInitialEnabled,
integrated: this.showIntegratedErrorTracking && this.initialIntegrated,
project: this.initialProject,
token: this.initialToken,
listProjectsEndpoint: this.listProjectsEndpoint,
@ -104,21 +142,41 @@ export default {
handleSubmit() {
this.updateSettings();
},
dismissAlert() {
this.isAlertDismissed = true;
},
},
};
</script>
<template>
<div>
<gl-alert v-if="showIntegratedTrackingDisabledAlert" variant="danger" @dismiss="dismissAlert">
<gl-sprintf :message="this.$options.i18n.integratedErrorTrackingDisabledText">
<template #epicLink="{ content }">
<gl-link :href="$options.epicLink" target="_blank">{{ content }}</gl-link>
</template>
<template #flagLink="{ content }">
<gl-link :href="$options.featureFlagLink" target="_blank">{{ content }}</gl-link>
</template>
</gl-sprintf>
</gl-alert>
<gl-form-group
:label="s__('ErrorTracking|Enable error tracking')"
label-for="error-tracking-enabled"
>
<gl-form-checkbox id="error-tracking-enabled" :checked="enabled" @change="updateEnabled">
<gl-form-checkbox
id="error-tracking-enabled"
:checked="enabled"
data-testid="error-tracking-enabled"
@change="updateEnabled"
>
{{ s__('ErrorTracking|Active') }}
</gl-form-checkbox>
</gl-form-group>
<gl-form-group
v-if="showIntegratedErrorTracking"
:label="s__('ErrorTracking|Error tracking backend')"
data-testid="tracking-backend-settings"
>

View File

@ -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.',
),
};

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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 ||= /(?<issue>\b#{Gitlab::Regex.jira_issue_key_regex})/

View File

@ -48,10 +48,6 @@ module Integrations
%w()
end
def self.supported_event_actions
%w()
end
def fields
[
{

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -0,0 +1 @@
8a0e80b6df1d942e5ec23641c935103cddd96c044e2a960b88bb38284cf4d070

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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**.

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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 ""

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

81
rubocop/todo_dir.rb Normal file
View File

@ -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

View File

@ -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,

View File

@ -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) }

View File

@ -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 {

View File

@ -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;

View File

@ -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);
},
);
});
});
});

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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) }

View File

@ -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)

View File

@ -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

View File

@ -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 }

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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