Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.rubocop.yml9
-rw-r--r--.rubocop_todo/cop/experiments_test_coverage.yml8
-rw-r--r--app/assets/javascripts/ci/reports/components/report_item.vue2
-rw-r--r--app/assets/javascripts/import_entities/components/group_dropdown.vue2
-rw-r--r--app/assets/javascripts/import_entities/components/import_target_dropdown.vue119
-rw-r--r--app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue40
-rw-r--r--doc/ci/testing/test_coverage_visualization.md1
-rw-r--r--qa/qa/page/admin/menu.rb6
-rw-r--r--qa/qa/page/project/import/github.rb4
-rw-r--r--rubocop/cop/experiments_test_coverage.rb114
-rw-r--r--spec/frontend/import_entities/components/import_target_dropdown_spec.js99
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js30
-rw-r--r--spec/frontend/import_entities/mock_data.js34
-rw-r--r--spec/lib/backup/database_spec.rb2
-rw-r--r--spec/rubocop/cop/experiments_test_coverage_spec.rb169
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb2
-rw-r--r--spec/tasks/gitlab/db_rake_spec.rb2
17 files changed, 597 insertions, 46 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 25c3b79f522..48e7a92d05f 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -1043,3 +1043,12 @@ Search/AvoidCheckingFinishedOnDeprecatedMigrations:
- 'ee/lib/elastic/**/*.rb'
- 'ee/lib/gitlab/elastic/**/*.rb'
- 'ee/spec/support/helpers/elasticsearch_helpers.rb'
+
+# See https://gitlab.com/gitlab-org/gitlab/-/issues/407233
+Cop/ExperimentsTestCoverage:
+ Enabled: true
+ Include:
+ - 'app/**/*'
+ - 'lib/**/*'
+ - 'ee/app/**/*'
+ - 'ee/lib/**/*'
diff --git a/.rubocop_todo/cop/experiments_test_coverage.yml b/.rubocop_todo/cop/experiments_test_coverage.yml
new file mode 100644
index 00000000000..a64ea13b743
--- /dev/null
+++ b/.rubocop_todo/cop/experiments_test_coverage.yml
@@ -0,0 +1,8 @@
+---
+Cop/ExperimentsTestCoverage:
+ Details: grace period
+ Exclude:
+ - 'app/controllers/groups/boards_controller.rb'
+ - 'app/controllers/projects/boards_controller.rb'
+ - 'app/experiments/build_ios_app_guide_email_experiment.rb'
+ - 'ee/app/views/shared/_tier_badge.html.haml.rb'
diff --git a/app/assets/javascripts/ci/reports/components/report_item.vue b/app/assets/javascripts/ci/reports/components/report_item.vue
index ca1f3301691..7342df6da67 100644
--- a/app/assets/javascripts/ci/reports/components/report_item.vue
+++ b/app/assets/javascripts/ci/reports/components/report_item.vue
@@ -53,7 +53,7 @@ export default {
};
</script>
<template>
- <li class="report-block-list-issue gl-p-3!" data-qa-selector="report_item_row">
+ <li class="report-block-list-issue gl-p-3!" data-testid="report-item-row">
<component
:is="iconComponent"
v-if="showReportSectionStatusIcon"
diff --git a/app/assets/javascripts/import_entities/components/group_dropdown.vue b/app/assets/javascripts/import_entities/components/group_dropdown.vue
index 1c31c04a416..68bdcf7ef90 100644
--- a/app/assets/javascripts/import_entities/components/group_dropdown.vue
+++ b/app/assets/javascripts/import_entities/components/group_dropdown.vue
@@ -9,6 +9,8 @@ import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/cons
import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+// This is added outside the component as each dropdown on the page triggers a query,
+// so if multiple queries fail, we only show 1 error.
const reportNamespaceLoadError = debounce(
() =>
createAlert({
diff --git a/app/assets/javascripts/import_entities/components/import_target_dropdown.vue b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
new file mode 100644
index 00000000000..b18a106608a
--- /dev/null
+++ b/app/assets/javascripts/import_entities/components/import_target_dropdown.vue
@@ -0,0 +1,119 @@
+<script>
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { debounce } from 'lodash';
+
+import { __, s__ } from '~/locale';
+import { createAlert } from '~/alert';
+import { truncate } from '~/lib/utils/text_utility';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { MINIMUM_SEARCH_LENGTH } from '~/graphql_shared/constants';
+import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
+
+// This is added outside the component as each dropdown on the page triggers a query,
+// so if multiple queries fail, we only show 1 error.
+const reportNamespaceLoadError = debounce(
+ () =>
+ createAlert({
+ message: s__('ImportProjects|Requesting namespaces failed'),
+ }),
+ DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
+);
+
+export default {
+ components: {
+ GlCollapsibleListbox,
+ },
+
+ props: {
+ selected: {
+ type: String,
+ required: true,
+ },
+ userNamespace: {
+ type: String,
+ required: true,
+ },
+ },
+
+ MAX_IMPORT_TARGET_LENGTH: 24,
+
+ data() {
+ return {
+ searchTerm: '',
+ };
+ },
+
+ apollo: {
+ namespaces: {
+ query: searchNamespacesWhereUserCanImportProjectsQuery,
+ variables() {
+ return {
+ search: this.searchTerm,
+ };
+ },
+ skip() {
+ const hasNotEnoughSearchCharacters =
+ this.searchTerm.length > 0 && this.searchTerm.length < MINIMUM_SEARCH_LENGTH;
+ return hasNotEnoughSearchCharacters;
+ },
+ update(data) {
+ return data.currentUser.groups.nodes;
+ },
+ error: reportNamespaceLoadError,
+ debounce: DEBOUNCE_DELAY,
+ },
+ },
+
+ computed: {
+ filteredNamespaces() {
+ return (this.namespaces ?? []).filter((ns) =>
+ ns.fullPath.toLowerCase().includes(this.searchTerm.toLowerCase()),
+ );
+ },
+
+ toggleText() {
+ return truncate(this.selected, this.$options.MAX_IMPORT_TARGET_LENGTH);
+ },
+
+ items() {
+ return [
+ {
+ text: __('Users'),
+ options: [{ text: this.userNamespace, value: this.userNamespace }],
+ },
+ {
+ text: __('Groups'),
+ options: this.filteredNamespaces.map((namespace) => {
+ return { text: namespace.fullPath, value: namespace.fullPath };
+ }),
+ },
+ ];
+ },
+ },
+
+ methods: {
+ onSelect(value) {
+ this.$emit('select', value);
+ },
+
+ onSearch(value) {
+ this.searchTerm = value.trim();
+ },
+ },
+};
+</script>
+
+<template>
+ <gl-collapsible-listbox
+ :items="items"
+ :selected="selected"
+ :toggle-text="toggleText"
+ searchable
+ fluid-width
+ toggle-class="gl-rounded-top-right-none! gl-rounded-bottom-right-none!"
+ data-qa-selector="target_namespace_selector_dropdown"
+ @select="onSelect"
+ @search="onSearch"
+ />
+</template>
diff --git a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
index 735939f991f..ed390f5ef58 100644
--- a/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
+++ b/app/assets/javascripts/import_entities/import_projects/components/provider_repo_table_row.vue
@@ -5,9 +5,6 @@ import {
GlFormInput,
GlButton,
GlLink,
- GlDropdownItem,
- GlDropdownDivider,
- GlDropdownSectionHeader,
GlTooltip,
GlSprintf,
GlTooltipDirective,
@@ -15,7 +12,7 @@ import {
import { mapState, mapGetters, mapActions } from 'vuex';
import { __ } from '~/locale';
import { helpPagePath } from '~/helpers/help_page_helper';
-import ImportGroupDropdown from '../../components/group_dropdown.vue';
+import ImportTargetDropdown from '../../components/import_target_dropdown.vue';
import ImportStatus from '../../components/import_status.vue';
import { STATUSES } from '../../constants';
import { isProjectImportable, isImporting, isIncompatible, getImportStatus } from '../utils';
@@ -23,13 +20,10 @@ import { isProjectImportable, isImporting, isIncompatible, getImportStatus } fro
export default {
name: 'ProviderRepoTableRow',
components: {
- ImportGroupDropdown,
ImportStatus,
+ ImportTargetDropdown,
GlFormInput,
GlButton,
- GlDropdownItem,
- GlDropdownDivider,
- GlDropdownSectionHeader,
GlIcon,
GlBadge,
GlLink,
@@ -151,6 +145,10 @@ export default {
});
}
},
+
+ onSelect(value) {
+ this.updateImportTarget({ targetNamespace: value });
+ },
},
helpUrl: helpPagePath('/user/project/import/github.md'),
@@ -188,27 +186,13 @@ export default {
<template v-if="repo.importSource.target">{{ repo.importSource.target }}</template>
<template v-else-if="isImportNotStarted || isSelectedForReimport">
<div class="gl-display-flex gl-align-items-stretch gl-w-full">
- <import-group-dropdown #default="{ namespaces }" :text="importTarget.targetNamespace">
- <template v-if="namespaces.length">
- <gl-dropdown-section-header>{{ __('Groups') }}</gl-dropdown-section-header>
- <gl-dropdown-item
- v-for="ns in namespaces"
- :key="ns.fullPath"
- data-qa-selector="target_group_dropdown_item"
- :data-qa-group-name="ns.fullPath"
- @click="updateImportTarget({ targetNamespace: ns.fullPath })"
- >
- {{ ns.fullPath }}
- </gl-dropdown-item>
- <gl-dropdown-divider />
- </template>
- <gl-dropdown-section-header>{{ __('Users') }}</gl-dropdown-section-header>
- <gl-dropdown-item @click="updateImportTarget({ targetNamespace: userNamespace })">{{
- userNamespace
- }}</gl-dropdown-item>
- </import-group-dropdown>
+ <import-target-dropdown
+ :selected="importTarget.targetNamespace"
+ :user-namespace="userNamespace"
+ @select="onSelect"
+ />
<div
- class="gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1"
+ class="gl-px-3 gl-display-flex gl-align-items-center gl-border-solid gl-border-0 gl-border-t-1 gl-border-b-1 gl-border-gray-400"
>
/
</div>
diff --git a/doc/ci/testing/test_coverage_visualization.md b/doc/ci/testing/test_coverage_visualization.md
index 669157daa21..613cefe1a0d 100644
--- a/doc/ci/testing/test_coverage_visualization.md
+++ b/doc/ci/testing/test_coverage_visualization.md
@@ -431,6 +431,7 @@ the coverage report itself and verify that:
to match the files in your repository.
- The pipeline has completed. If the pipeline is [blocked on a manual job](../jobs/job_control.md#types-of-manual-jobs),
the pipeline is not considered complete.
+- The coverage report file does not exceed the [limits](#limits).
Report artifacts are not downloadable by default. If you want the report to be downloadable
from the job details page, add your coverage report to the artifact `paths`:
diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb
index f58d672efe8..3b044f8a051 100644
--- a/qa/qa/page/admin/menu.rb
+++ b/qa/qa/page/admin/menu.rb
@@ -67,6 +67,12 @@ module QA
click_element :admin_overview_groups_link
end
+ def go_to_security_and_compliance
+ hover_element(:admin_settings_menu_link) do
+ click_element :admin_security_and_compliance_link
+ end
+ end
+
def go_to_applications
return click_element(:nav_item_link, submenu_item: 'Applications') if Runtime::Env.super_sidebar_enabled?
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
index 898bf78b46a..57c6484dc13 100644
--- a/qa/qa/page/project/import/github.rb
+++ b/qa/qa/page/project/import/github.rb
@@ -19,7 +19,7 @@ module QA
element :import_status_indicator
end
- view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do
+ view "app/assets/javascripts/import_entities/components/import_target_dropdown.vue" do
element :target_namespace_selector_dropdown
end
@@ -47,7 +47,7 @@ module QA
def import!(gh_project_name, target_group_path, project_name)
within_element(:project_import_row, source_project: gh_project_name) do
click_element(:target_namespace_selector_dropdown)
- click_element(:target_group_dropdown_item, group_name: target_group_path)
+ click_element(:"listbox-item-#{target_group_path}", wait: 10)
fill_element(:project_path_field, project_name)
retry_until do
diff --git a/rubocop/cop/experiments_test_coverage.rb b/rubocop/cop/experiments_test_coverage.rb
new file mode 100644
index 00000000000..4bb2832030c
--- /dev/null
+++ b/rubocop/cop/experiments_test_coverage.rb
@@ -0,0 +1,114 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Check for test coverage for GitLab experiments.
+ class ExperimentsTestCoverage < RuboCop::Cop::Base
+ CLASS_OFFENSE = 'Make sure experiment class has test coverage for all the variants.'
+ BLOCK_OFFENSE = 'Make sure experiment block has test coverage for all the variants.'
+
+ # Validates classes inherited from ApplicationExperiment
+ # These classes are located under app/experiments or ee/app/experiments
+ def on_class(node)
+ return if node.parent_class&.const_name != 'ApplicationExperiment'
+ return if covered_with_tests?(node)
+
+ add_offense(node, message: CLASS_OFFENSE)
+ end
+
+ # Validates experiments block in *.rb and *.haml files:
+ # experiment(:experiment_name) do |e|
+ # e.candidate { 'candidate' }
+ # e.run
+ # end
+ def on_block(node)
+ return if node.method_name != :experiment
+ return if covered_with_tests?(node)
+
+ add_offense(node, message: BLOCK_OFFENSE)
+ end
+
+ private
+
+ def covered_with_tests?(node)
+ tests_code = test_files_code(node)
+
+ return false if tests_code.blank?
+ return false unless tests_code.match?(stub_experiments_matcher)
+ return false unless tests_code.include?(experiment_name(node))
+
+ experiment_variants(node).map { |variant| tests_code.include?(variant) }.all?(&:present?)
+ end
+
+ def test_files_code(node)
+ # haml-lint add .rb extension to *.haml files
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/415330#caveats
+ test_file_path = filepath(node).gsub('app/', 'spec/').gsub('.rb', '_spec.rb')
+ "#{read_file(test_file_path)}\n#{additional_tests_code(test_file_path)}"
+ end
+
+ def additional_tests_code(test_file_path)
+ # rubocop:disable Gitlab/NoCodeCoverageComment
+ # :nocov: File paths stubed in tests
+ if test_file_path.include?('/controllers/')
+ read_file(test_file_path.gsub('/controllers/', '/requests/'))
+ elsif test_file_path.include?('/lib/api/')
+ read_file(test_file_path.gsub('/lib/', '/spec/requests/'))
+ end
+ # :nocov:
+ # rubocop:enable Gitlab/NoCodeCoverageComment
+ end
+
+ def read_file(file_path)
+ File.exist?(file_path) ? File.new(file_path).read : ''
+ end
+
+ def experiment_name(node)
+ if node.is_a?(RuboCop::AST::ClassNode)
+ File.basename(filepath(node), '_experiment.rb')
+ else
+ block_node_value(node)
+ end
+ end
+
+ def experiment_variants(node)
+ node.body.children.filter_map do |child|
+ next unless child.is_a?(RuboCop::AST::SendNode) || child.is_a?(RuboCop::AST::BlockNode)
+
+ extract_variant(child)
+ end
+ end
+
+ def extract_variant(node)
+ # control enabled by default for tests
+ case node.method_name
+ when :candidate then 'candidate'
+ when :variant then variant_name(node)
+ end
+ end
+
+ def variant_name(node)
+ return send_node_value(node) if node.is_a?(RuboCop::AST::SendNode)
+
+ block_node_value(node)
+ end
+
+ def block_node_value(node)
+ send_node_value(node.children[0])
+ end
+
+ def send_node_value(node)
+ node.children[2].value.to_s
+ end
+
+ def filepath(node)
+ node.location.expression.source_buffer.name
+ end
+
+ def stub_experiments_matcher
+ # validates test files contains uncommented stub_experiments(...
+ /^([^#]|\s*|\w*)stub_experiments\(/
+ end
+ end
+ end
+end
diff --git a/spec/frontend/import_entities/components/import_target_dropdown_spec.js b/spec/frontend/import_entities/components/import_target_dropdown_spec.js
new file mode 100644
index 00000000000..c12baed2374
--- /dev/null
+++ b/spec/frontend/import_entities/components/import_target_dropdown_spec.js
@@ -0,0 +1,99 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import ImportTargetDropdown from '~/import_entities/components/import_target_dropdown.vue';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
+
+import { mockAvailableNamespaces, mockNamespacesResponse, mockUserNamespace } from '../mock_data';
+
+Vue.use(VueApollo);
+
+describe('ImportTargetDropdown', () => {
+ let wrapper;
+
+ const defaultProps = {
+ selected: mockUserNamespace,
+ userNamespace: mockUserNamespace,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ const apolloProvider = createMockApollo([
+ [
+ searchNamespacesWhereUserCanImportProjectsQuery,
+ jest.fn().mockResolvedValue(mockNamespacesResponse),
+ ],
+ ]);
+
+ wrapper = shallowMount(ImportTargetDropdown, {
+ apolloProvider,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxUsersItems = () => findListbox().props('items')[0].options;
+ const findListboxGroupsItems = () => findListbox().props('items')[1].options;
+
+ const waitForQuery = async () => {
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await waitForPromises();
+ };
+
+ it('renders listbox', () => {
+ createComponent();
+
+ expect(findListbox().exists()).toBe(true);
+ });
+
+ it('truncates "toggle-text" when "selected" is too long', () => {
+ const mockSelected = 'a-group-path-that-is-longer-than-24-characters';
+
+ createComponent({
+ props: { selected: mockSelected },
+ });
+
+ expect(findListbox().props('toggleText')).toBe('a-group-path-that-is-lo…');
+ });
+
+ it('passes userNamespace as "Users" group item', () => {
+ createComponent();
+
+ expect(findListboxUsersItems()).toEqual([
+ { text: mockUserNamespace, value: mockUserNamespace },
+ ]);
+ });
+
+ it('passes namespaces from GraphQL as "Groups" group item', async () => {
+ createComponent();
+
+ await waitForQuery();
+
+ expect(findListboxGroupsItems()).toEqual(
+ mockAvailableNamespaces.map((namespace) => ({
+ text: namespace.fullPath,
+ value: namespace.fullPath,
+ })),
+ );
+ });
+
+ it('filters namespaces based on user input', async () => {
+ createComponent();
+
+ findListbox().vm.$emit('search', 'match');
+
+ await waitForQuery();
+
+ expect(findListboxGroupsItems()).toEqual([
+ { text: 'match1', value: 'match1' },
+ { text: 'match2', value: 'match2' },
+ ]);
+ });
+});
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index 57e232a4c46..9e4838ed020 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -1,9 +1,10 @@
import { GlBadge, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { STATUSES } from '~/import_entities//constants';
-import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import { STATUSES } from '~/import_entities/constants';
+import ImportTargetDropdown from '~/import_entities/components/import_target_dropdown.vue';
import ImportStatus from '~/import_entities/components/import_status.vue';
import ProviderRepoTableRow from '~/import_entities/import_projects/components/provider_repo_table_row.vue';
@@ -39,8 +40,9 @@ describe('ProviderRepoTableRow', () => {
const findImportButton = () => findButton('Import');
const findReimportButton = () => findButton('Re-import');
- const findGroupDropdown = () => wrapper.findComponent(ImportGroupDropdown);
+ const findImportTargetDropdown = () => wrapper.findComponent(ImportTargetDropdown);
const findImportStatus = () => wrapper.findComponent(ImportStatus);
+ const findProviderLink = () => wrapper.findByTestId('providerLink');
const findCancelButton = () => {
const buttons = wrapper
@@ -55,7 +57,7 @@ describe('ProviderRepoTableRow', () => {
const store = initStore();
- wrapper = shallowMount(ProviderRepoTableRow, {
+ wrapper = shallowMountExtended(ProviderRepoTableRow, {
store,
propsData: { userNamespace, optionalStages: {}, ...props },
});
@@ -75,7 +77,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders project information', () => {
- const providerLink = wrapper.find('[data-testid=providerLink]');
+ const providerLink = findProviderLink();
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
@@ -86,7 +88,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders a group namespace select', () => {
- expect(wrapper.findComponent(ImportGroupDropdown).exists()).toBe(true);
+ expect(findImportTargetDropdown().exists()).toBe(true);
});
it('renders import button', () => {
@@ -106,7 +108,11 @@ describe('ProviderRepoTableRow', () => {
it('includes optionalStages to import', async () => {
const OPTIONAL_STAGES = { stage1: true, stage2: false };
- await wrapper.setProps({ optionalStages: OPTIONAL_STAGES });
+
+ mountComponent({
+ repo,
+ optionalStages: OPTIONAL_STAGES,
+ });
findImportButton().vm.$emit('click');
@@ -192,7 +198,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders project information', () => {
- const providerLink = wrapper.find('[data-testid=providerLink]');
+ const providerLink = findProviderLink();
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
@@ -203,7 +209,7 @@ describe('ProviderRepoTableRow', () => {
});
it('does not render a namespace select', () => {
- expect(findGroupDropdown().exists()).toBe(false);
+ expect(findImportTargetDropdown().exists()).toBe(false);
});
it('does not render import button', () => {
@@ -219,7 +225,7 @@ describe('ProviderRepoTableRow', () => {
await nextTick();
- expect(findGroupDropdown().exists()).toBe(true);
+ expect(findImportTargetDropdown().exists()).toBe(true);
});
it('imports repo when clicking re-import button', async () => {
@@ -282,7 +288,7 @@ describe('ProviderRepoTableRow', () => {
});
it('renders project information', () => {
- const providerLink = wrapper.find('[data-testid=providerLink]');
+ const providerLink = findProviderLink();
expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
expect(providerLink.text()).toMatch(repo.importSource.fullName);
diff --git a/spec/frontend/import_entities/mock_data.js b/spec/frontend/import_entities/mock_data.js
new file mode 100644
index 00000000000..9208f99651f
--- /dev/null
+++ b/spec/frontend/import_entities/mock_data.js
@@ -0,0 +1,34 @@
+const mockGroupFactory = (fullPath) => ({
+ id: `gid://gitlab/Group/${fullPath}`,
+ fullPath,
+ name: fullPath,
+ visibility: 'public',
+ webUrl: `http://gdk.test:3000/groups/${fullPath}`,
+ __typename: 'Group',
+});
+
+export const mockAvailableNamespaces = [
+ mockGroupFactory('match1'),
+ mockGroupFactory('unrelated'),
+ mockGroupFactory('match2'),
+];
+
+export const mockNamespacesResponse = {
+ data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ groups: {
+ nodes: mockAvailableNamespaces,
+ __typename: 'GroupConnection',
+ },
+ namespace: {
+ id: 'gid://gitlab/Namespaces::UserNamespace/1',
+ fullPath: 'root',
+ __typename: 'Namespace',
+ },
+ __typename: 'UserCore',
+ },
+ },
+};
+
+export const mockUserNamespace = 'user1';
diff --git a/spec/lib/backup/database_spec.rb b/spec/lib/backup/database_spec.rb
index 5d53ad9fe22..61e6c59a1a5 100644
--- a/spec/lib/backup/database_spec.rb
+++ b/spec/lib/backup/database_spec.rb
@@ -22,7 +22,7 @@ RSpec.describe Backup::Database, feature_category: :backup_restore do
end
end
- before_all do
+ before(:all) do # rubocop:disable RSpec/BeforeAll
Rake::Task.define_task(:environment)
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/gitlab/backup'
diff --git a/spec/rubocop/cop/experiments_test_coverage_spec.rb b/spec/rubocop/cop/experiments_test_coverage_spec.rb
new file mode 100644
index 00000000000..eb1e672ef40
--- /dev/null
+++ b/spec/rubocop/cop/experiments_test_coverage_spec.rb
@@ -0,0 +1,169 @@
+# frozen_string_literal: true
+
+require 'rubocop_spec_helper'
+
+require_relative '../../../rubocop/cop/experiments_test_coverage'
+
+RSpec.describe RuboCop::Cop::ExperimentsTestCoverage, feature_category: :experimentation_conversion do
+ let(:class_offense) { described_class::CLASS_OFFENSE }
+ let(:block_offense) { described_class::BLOCK_OFFENSE }
+
+ before do
+ allow(File).to receive(:exist?).and_return(true)
+ allow(File).to receive(:new).and_return(instance_double(File, read: tests_code))
+ end
+
+ describe '#on_class' do
+ context 'when there are no tests' do
+ let(:tests_code) { '' }
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class ExperimentName < ApplicationExperiment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
+ end
+ RUBY
+ end
+ end
+
+ context 'when there is no stub_experiments' do
+ let(:tests_code) { "candidate third" }
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class ExperimentName < ApplicationExperiment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
+ candidate
+ variant(:third) { 'third option' }
+ end
+ RUBY
+ end
+ end
+
+ context 'when variant test is missing' do
+ let(:tests_code) { "\nstub_experiments(experiment_name: :candidate)" }
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class ExperimentName < ApplicationExperiment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
+ candidate
+ variant(:third) { 'third option' }
+ end
+ RUBY
+ end
+ end
+
+ context 'when stub_experiments is commented out' do
+ let(:tests_code) do
+ "\n# stub_experiments(experiment_name: :candidate, experiment_name: :third)"
+ end
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ class ExperimentName < ApplicationExperiment
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{class_offense}
+ candidate
+ variant(:third) { 'third option' }
+ end
+ RUBY
+ end
+ end
+
+ context 'when all tests are present' do
+ let(:tests_code) do
+ "#\nstub_experiments(experiment_name: :candidate, experiment_name: :third)"
+ end
+
+ before do
+ allow(cop).to receive(:filepath).and_return('app/experiments/experiment_name_experiment.rb')
+ end
+
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ class ExperimentName < ApplicationExperiment
+ candidate
+ variant(:third) { 'third option' }
+ end
+ RUBY
+ end
+ end
+ end
+
+ describe '#on_block' do
+ context 'when there are no tests' do
+ let(:tests_code) { '' }
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ experiment(:experiment_name) do |e|
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
+ end
+ RUBY
+ end
+ end
+
+ context 'when there is no stub_experiments' do
+ let(:tests_code) { "candidate third" }
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ experiment(:experiment_name) do |e|
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
+ e.candidate { 'candidate' }
+ e.variant(:third) { 'third option' }
+ e.run
+ end
+ RUBY
+ end
+ end
+
+ context 'when variant test is missing' do
+ let(:tests_code) { "\nstub_experiments(experiment_name: :candidate)" }
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ experiment(:experiment_name) do |e|
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
+ e.candidate { 'candidate' }
+ e.variant(:third) { 'third option' }
+ e.run
+ end
+ RUBY
+ end
+ end
+
+ context 'when stub_experiments is commented out' do
+ let(:tests_code) do
+ "\n# stub_experiments(experiment_name: :candidate, experiment_name: :third)"
+ end
+
+ it 'registers an offense' do
+ expect_offense(<<~RUBY)
+ experiment(:experiment_name) do |e|
+ ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #{block_offense}
+ e.candidate { 'candidate' }
+ e.variant(:third) { 'third option' }
+ e.run
+ end
+ RUBY
+ end
+ end
+
+ context 'when all tests are present' do
+ let(:tests_code) do
+ "#\nstub_experiments(experiment_name: :candidate, experiment_name: :third)"
+ end
+
+ it 'does not register an offense' do
+ expect_no_offenses(<<~RUBY)
+ experiment(:experiment_name) do |e|
+ e.candidate { 'candidate' }
+ e.variant(:third) { 'third option' }
+ e.run
+ end
+ RUBY
+ end
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 23df8d4ef85..414ce01917b 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -38,7 +38,7 @@ RSpec.describe 'gitlab:backup namespace rake tasks', :delete, feature_category:
%w[db repositories]
end
- before_all do
+ before(:all) do # rubocop:disable RSpec/BeforeAll
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/gitlab/backup'
Rake.application.rake_require 'tasks/gitlab/shell'
diff --git a/spec/tasks/gitlab/db_rake_spec.rb b/spec/tasks/gitlab/db_rake_spec.rb
index b12f2b839af..344429dc6ec 100644
--- a/spec/tasks/gitlab/db_rake_spec.rb
+++ b/spec/tasks/gitlab/db_rake_spec.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
require 'rake'
RSpec.describe 'gitlab:db namespace rake task', :silence_stdout, feature_category: :database do
- before_all do
+ before(:all) do # rubocop:disable RSpec/BeforeAll
Rake.application.rake_require 'active_record/railties/databases'
Rake.application.rake_require 'tasks/seed_fu'
Rake.application.rake_require 'tasks/gitlab/db'