- 1-6;
end
- 2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"];
+ 2_2-2["rspec-all frontend_fixture (7 minutes)"];
class 2_2-2 criticalPath;
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
2_2-4["memory-on-boot (3.5 minutes)"];
@@ -284,7 +284,7 @@ graph RL;
3_1-1["jest (14.5 minutes)"];
class 3_1-1 criticalPath;
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
- subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
+ subgraph "Needs `rspec-all frontend_fixture`";
3_1-1 --> 2_2-2;
end
@@ -355,7 +355,7 @@ graph RL;
2_1-1 & 2_1-2 & 2_1-3 & 2_1-4 --> 1-6;
end
- 2_2-2["rspec frontend_fixture/rspec-ee frontend_fixture (7 minutes)"];
+ 2_2-2["rspec-all frontend_fixture (7 minutes)"];
class 2_2-2 criticalPath;
click 2_2-2 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=7910143&udv=0"
2_2-4["memory-on-boot (3.5 minutes)"];
@@ -395,7 +395,7 @@ graph RL;
3_1-1["jest (14.5 minutes)"];
class 3_1-1 criticalPath;
click 3_1-1 "https://app.periscopedata.com/app/gitlab/652085/Engineering-Productivity---Pipeline-Build-Durations?widget=6914204&udv=0"
- subgraph "Needs `rspec frontend_fixture/rspec-ee frontend_fixture`";
+ subgraph "Needs `rspec-all frontend_fixture`";
3_1-1 --> 2_2-2;
end
diff --git a/doc/operations/metrics/dashboards/panel_types.md b/doc/operations/metrics/dashboards/panel_types.md
index dc96e2556ac..140e18b5b13 100644
--- a/doc/operations/metrics/dashboards/panel_types.md
+++ b/doc/operations/metrics/dashboards/panel_types.md
@@ -62,12 +62,10 @@ panel_groups:
query_range: 'http_requests_total'
label: '# of Requests'
unit: 'count'
- metrics:
- id: anomaly_requests_upper_limit
query_range: 10000
label: 'Max # of requests'
unit: 'count'
- metrics:
- id: anomaly_requests_lower_limit
query_range: 2000
label: 'Min # of requests'
diff --git a/doc/operations/metrics/dashboards/variables.md b/doc/operations/metrics/dashboards/variables.md
index 4b083284819..fa79524883d 100644
--- a/doc/operations/metrics/dashboards/variables.md
+++ b/doc/operations/metrics/dashboards/variables.md
@@ -8,7 +8,9 @@ info: To determine the technical writer assigned to the Stage/Group associated w
## Query variables
-Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"` ([added](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/20793) in GitLab 12.7.
+
+Variables can be specified using double curly braces, such as `"{{ci_environment_slug}}"`.
Support for the `"%{ci_environment_slug}"` format was
[removed](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31581) in GitLab 13.0.
@@ -66,5 +68,5 @@ avg(sum(container_memory_usage_bytes{container_name!="{{pod}}"}) by (job)) witho
The URL for this query would be:
```plaintext
-http://gitlab.com/
//-/environments//metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD
+https://gitlab.com///-/environments//metrics?dashboard=.gitlab%2Fdashboards%2Fcustom.yml&pod=POD
```
diff --git a/doc/operations/metrics/dashboards/yaml.md b/doc/operations/metrics/dashboards/yaml.md
index 45803598a40..8068a66d5c4 100644
--- a/doc/operations/metrics/dashboards/yaml.md
+++ b/doc/operations/metrics/dashboards/yaml.md
@@ -14,7 +14,7 @@ Dashboards have several components:
The following tables outline the details of expected properties.
-## **Dashboard (top-level) properties**
+## Dashboard (top-level) properties
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
@@ -23,7 +23,7 @@ The following tables outline the details of expected properties.
| `templating` | hash | no | Top level key under which templating related options can be added. |
| `links` | array | no | Add links to display on the dashboard. |
-## **Templating (`templating`) properties**
+## Templating (`templating`) properties
| Property | Type | Required | Description |
| -------- | ---- | -------- | ----------- |
@@ -31,7 +31,7 @@ The following tables outline the details of expected properties.
Read the documentation on [templating](templating_variables.md).
-## **Links (`links`) properties**
+## Links (`links`) properties
| Property | Type | Required | Description |
| -------- | ---- | -------- | ----------- |
@@ -41,7 +41,7 @@ Read the documentation on [templating](templating_variables.md).
Read the documentation on [links](index.md#add-related-links-to-custom-dashboards).
-## **Panel group (`panel_groups`) properties**
+## Panel group (`panel_groups`) properties
Dashboards display panel groups in the order they are listed in the dashboard YAML file.
@@ -55,7 +55,7 @@ is no longer used.
Panels in a panel group are laid out in rows consisting of two panels per row. An exception to this rule are single panels on a row: these panels take the full width of their containing row.
-## **Panel (`panels`) properties**
+## Panel (`panels`) properties
Dashboards display panels in the order they are listed in the dashboard YAML file.
@@ -72,7 +72,7 @@ is no longer used.
| `metrics` | array | yes | The metrics which should be displayed in the panel. Any number of metrics can be displayed when `type` is `area-chart` or `line-chart`, whereas only 3 can be displayed when `type` is `anomaly-chart`. |
| `links` | array | no | Add links to display on the chart's [context menu](index.md#chart-context-menu). |
-## **Axis (`panels[].y_axis`) properties**
+## Axis (`panels[].y_axis`) properties
| Property | Type | Required | Description |
| ----------- | ------ | ----------------------------- | -------------------------------------------------------------------- |
@@ -80,7 +80,7 @@ is no longer used.
| `format` | string | no, defaults to `engineering` | Unit format used. See the [full list of units](yaml_number_format.md). |
| `precision` | number | no, defaults to `2` | Number of decimal places to display in the number. | |
-## **Metrics (`metrics`) properties**
+## Metrics (`metrics`) properties
| Property | Type | Required | Description |
| ------ | ------ | ------ | ------ |
diff --git a/lib/gitlab/ci/pipeline/seed/build.rb b/lib/gitlab/ci/pipeline/seed/build.rb
index 934bf22d8ad..9ad5d6538b7 100644
--- a/lib/gitlab/ci/pipeline/seed/build.rb
+++ b/lib/gitlab/ci/pipeline/seed/build.rb
@@ -106,10 +106,15 @@ module Gitlab
environment = Seed::Environment.new(build).to_resource
- # If there is a validation error on environment creation, such as
- # the name contains invalid character, the build falls back to a
- # non-environment job.
unless environment.persisted?
+ if Feature.enabled?(:surface_environment_creation_failure, build.project, default_enabled: :yaml) &&
+ Feature.disabled?(:surface_environment_creation_failure_override, build.project)
+ return { status: :failed, failure_reason: :environment_creation_failure }
+ end
+
+ # If there is a validation error on environment creation, such as
+ # the name contains invalid character, the build falls back to a
+ # non-environment job.
Gitlab::ErrorTracking.track_exception(
EnvironmentCreationFailure.new,
project_id: build.project_id,
diff --git a/lib/gitlab/ci/status/build/failed.rb b/lib/gitlab/ci/status/build/failed.rb
index ee210e51232..b0f12ff7517 100644
--- a/lib/gitlab/ci/status/build/failed.rb
+++ b/lib/gitlab/ci/status/build/failed.rb
@@ -33,7 +33,8 @@ module Gitlab
ci_quota_exceeded: 'no more CI minutes available',
no_matching_runner: 'no matching runner available',
trace_size_exceeded: 'log size limit exceeded',
- builds_disabled: 'project builds are disabled'
+ builds_disabled: 'project builds are disabled',
+ environment_creation_failure: 'environment creation failure'
}.freeze
private_constant :REASONS
diff --git a/lib/gitlab/database/load_balancing/load_balancer.rb b/lib/gitlab/database/load_balancing/load_balancer.rb
index a51626b895a..5d9dbfdabbd 100644
--- a/lib/gitlab/database/load_balancing/load_balancer.rb
+++ b/lib/gitlab/database/load_balancing/load_balancer.rb
@@ -30,6 +30,10 @@ module Gitlab
end
end
+ def primary_only?
+ @primary_only
+ end
+
def disconnect!(timeout: 120)
host_list.hosts.each { |host| host.disconnect!(timeout: timeout) }
end
@@ -151,6 +155,17 @@ module Gitlab
# Yields a block, retrying it upon error using an exponential backoff.
def retry_with_backoff(retries = 3, time = 2)
+ # In CI we only use the primary, but databases may not always be
+ # available (or take a few seconds to become available). Retrying in
+ # this case can slow down CI jobs. In addition, retrying with _only_
+ # a primary being present isn't all that helpful.
+ #
+ # To prevent this from happening, we don't make any attempt at
+ # retrying unless one or more replicas are used. This matches the
+ # behaviour from before we enabled load balancing code even if no
+ # replicas were configured.
+ return yield if primary_only?
+
retried = 0
last_error = nil
@@ -176,6 +191,11 @@ module Gitlab
def connection_error?(error)
case error
+ when ActiveRecord::NoDatabaseError
+ # Retrying this error isn't going to magically make the database
+ # appear. It also slows down CI jobs that are meant to create the
+ # database in the first place.
+ false
when ActiveRecord::StatementInvalid, ActionView::Template::Error
# After connecting to the DB Rails will wrap query errors using this
# class.
diff --git a/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb b/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb
index e5a50c92329..b8de7de848d 100644
--- a/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb
+++ b/lib/gitlab/usage_data_counters/ci_template_unique_counter.rb
@@ -5,23 +5,14 @@ module Gitlab::UsageDataCounters
REDIS_SLOT = 'ci_templates'
KNOWN_EVENTS_FILE_PATH = File.expand_path('known_events/ci_templates.yml', __dir__)
- # NOTE: Events originating from implicit Auto DevOps pipelines get prefixed with `implicit_`
- TEMPLATE_TO_EVENT = {
- '5-Minute-Production-App.gitlab-ci.yml' => '5_min_production_app',
- 'Auto-DevOps.gitlab-ci.yml' => 'auto_devops',
- 'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' => 'aws_cf_deploy_ec2',
- 'AWS/Deploy-ECS.gitlab-ci.yml' => 'aws_deploy_ecs',
- 'Jobs/Build.gitlab-ci.yml' => 'auto_devops_build',
- 'Jobs/Deploy.gitlab-ci.yml' => 'auto_devops_deploy',
- 'Jobs/Deploy.latest.gitlab-ci.yml' => 'auto_devops_deploy_latest',
- 'Security/SAST.gitlab-ci.yml' => 'security_sast',
- 'Security/Secret-Detection.gitlab-ci.yml' => 'security_secret_detection',
- 'Terraform/Base.latest.gitlab-ci.yml' => 'terraform_base_latest'
- }.freeze
-
class << self
def track_unique_project_event(project_id:, template:, config_source:)
- Gitlab::UsageDataCounters::HLLRedisCounter.track_event(ci_template_event_name(template, config_source), values: project_id)
+ expanded_template_name = expand_template_name(template)
+ return unless expanded_template_name
+
+ Gitlab::UsageDataCounters::HLLRedisCounter.track_event(
+ ci_template_event_name(expanded_template_name, config_source), values: project_id
+ )
end
def ci_templates(relative_base = 'lib/gitlab/ci/templates')
@@ -30,9 +21,12 @@ module Gitlab::UsageDataCounters
def ci_template_event_name(template_name, config_source)
prefix = 'implicit_' if config_source.to_s == 'auto_devops_source'
- template_event_name = TEMPLATE_TO_EVENT[template_name] || template_to_event_name(template_name)
- "p_#{REDIS_SLOT}_#{prefix}#{template_event_name}"
+ "p_#{REDIS_SLOT}_#{prefix}#{template_to_event_name(template_name)}"
+ end
+
+ def expand_template_name(template_name)
+ Gitlab::Template::GitlabCiYmlTemplate.find(template_name.chomp('.gitlab-ci.yml'))&.full_name
end
private
diff --git a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
index cf790767f17..99bdd3ca9e9 100644
--- a/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
+++ b/lib/gitlab/usage_data_counters/known_events/ci_templates.yml
@@ -1,44 +1,8 @@
-# Implicit Auto DevOps pipeline events
-- name: p_ci_templates_implicit_auto_devops
- category: ci_templates
- redis_slot: ci_templates
- aggregation: weekly
-
-# Explicit include:template pipeline events
-- name: p_ci_templates_5_min_production_app
- category: ci_templates
- redis_slot: ci_templates
- aggregation: weekly
-
-- name: p_ci_templates_aws_cf_deploy_ec2
- category: ci_templates
- redis_slot: ci_templates
- aggregation: weekly
-
-- name: p_ci_templates_auto_devops_build
- category: ci_templates
- redis_slot: ci_templates
- aggregation: weekly
-
-- name: p_ci_templates_auto_devops_deploy
- category: ci_templates
- redis_slot: ci_templates
- aggregation: weekly
-
-- name: p_ci_templates_auto_devops_deploy_latest
- category: ci_templates
- redis_slot: ci_templates
- aggregation: weekly
-
-# This part of the file is generated automatically by
+# This file is generated automatically by
# bin/rake gitlab:usage_data:generate_ci_template_events
#
# Do not edit it manually!
-#
-# The section above this should be removed once we roll out tracking all ci
-# templates
-# https://gitlab.com/gitlab-org/gitlab/-/issues/339684
-
+---
- name: p_ci_templates_terraform_base_latest
category: ci_templates
redis_slot: ci_templates
@@ -463,6 +427,10 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
+- name: p_ci_templates_implicit_auto_devops
+ category: ci_templates
+ redis_slot: ci_templates
+ aggregation: weekly
- name: p_ci_templates_implicit_jobs_dast_default_branch_deploy
category: ci_templates
redis_slot: ci_templates
@@ -499,11 +467,11 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
-- name: p_ci_templates_implicit_auto_devops_deploy
+- name: p_ci_templates_implicit_jobs_deploy
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
-- name: p_ci_templates_implicit_auto_devops_build
+- name: p_ci_templates_implicit_jobs_build
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
@@ -515,7 +483,7 @@
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
-- name: p_ci_templates_implicit_auto_devops_deploy_latest
+- name: p_ci_templates_implicit_jobs_deploy_latest
category: ci_templates
redis_slot: ci_templates
aggregation: weekly
diff --git a/lib/tasks/gitlab/usage_data.rake b/lib/tasks/gitlab/usage_data.rake
index 35ddc627389..694c49240ed 100644
--- a/lib/tasks/gitlab/usage_data.rake
+++ b/lib/tasks/gitlab/usage_data.rake
@@ -41,20 +41,32 @@ namespace :gitlab do
repository_includes = ci_template_includes_hash(:repository_source)
auto_devops_jobs_includes = ci_template_includes_hash(:auto_devops_source, 'Jobs')
auto_devops_security_includes = ci_template_includes_hash(:auto_devops_source, 'Security')
- all_includes = [*repository_includes, *auto_devops_jobs_includes, *auto_devops_security_includes]
+ all_includes = [
+ *repository_includes,
+ ci_template_event('p_ci_templates_implicit_auto_devops'),
+ *auto_devops_jobs_includes,
+ *auto_devops_security_includes
+ ]
File.write(Gitlab::UsageDataCounters::CiTemplateUniqueCounter::KNOWN_EVENTS_FILE_PATH, banner + YAML.dump(all_includes).gsub(/ *$/m, ''))
end
def ci_template_includes_hash(source, template_directory = nil)
Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_templates("lib/gitlab/ci/templates/#{template_directory}").map do |template|
- {
- 'name' => Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_template_event_name("#{template_directory}/#{template}", source),
- 'category' => 'ci_templates',
- 'redis_slot' => Gitlab::UsageDataCounters::CiTemplateUniqueCounter::REDIS_SLOT,
- 'aggregation' => 'weekly'
- }
+ expanded_template_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.expand_template_name("#{template_directory}/#{template}")
+ event_name = Gitlab::UsageDataCounters::CiTemplateUniqueCounter.ci_template_event_name(expanded_template_name, source)
+
+ ci_template_event(event_name)
end
end
+
+ def ci_template_event(event_name)
+ {
+ 'name' => event_name,
+ 'category' => 'ci_templates',
+ 'redis_slot' => Gitlab::UsageDataCounters::CiTemplateUniqueCounter::REDIS_SLOT,
+ 'aggregation' => 'weekly'
+ }
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 6f48a77f9be..b750df89e83 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -13060,6 +13060,9 @@ msgstr ""
msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
msgstr ""
+msgid "Environments|Open"
+msgstr ""
+
msgid "Environments|Open live environment"
msgstr ""
@@ -25726,6 +25729,9 @@ msgstr ""
msgid "Prevent adding new members to project membership within this group"
msgstr ""
+msgid "Prevent auto-stopping"
+msgstr ""
+
msgid "Prevent editing approval rules in projects and merge requests."
msgstr ""
diff --git a/scripts/rspec_helpers.sh b/scripts/rspec_helpers.sh
index 797d9188f81..280a1586de3 100644
--- a/scripts/rspec_helpers.sh
+++ b/scripts/rspec_helpers.sh
@@ -109,14 +109,18 @@ function rspec_paralellized_job() {
local test_level="${job_name[1]}"
local report_name=$(echo "${CI_JOB_NAME}" | sed -E 's|[/ ]|_|g') # e.g. 'rspec unit pg12 1/24' would become 'rspec_unit_pg12_1_24'
local rspec_opts="${1}"
- local spec_folder_prefix=""
+ local spec_folder_prefixes=""
if [[ "${test_tool}" =~ "-ee" ]]; then
- spec_folder_prefix="ee/"
+ spec_folder_prefixes="'ee/'"
fi
if [[ "${test_tool}" =~ "-jh" ]]; then
- spec_folder_prefix="jh/"
+ spec_folder_prefixes="'jh/'"
+ fi
+
+ if [[ "${test_tool}" =~ "-all" ]]; then
+ spec_folder_prefixes="['', 'ee/']"
fi
export KNAPSACK_LOG_LEVEL="debug"
@@ -131,7 +135,7 @@ function rspec_paralellized_job() {
cp "${KNAPSACK_RSPEC_SUITE_REPORT_PATH}" "${KNAPSACK_REPORT_PATH}"
if [[ -z "${KNAPSACK_TEST_FILE_PATTERN}" ]]; then
- pattern=$(ruby -r./tooling/quality/test_level.rb -e "puts Quality::TestLevel.new(%(${spec_folder_prefix})).pattern(:${test_level})")
+ pattern=$(ruby -r./tooling/quality/test_level.rb -e "puts Quality::TestLevel.new(${spec_folder_prefixes}).pattern(:${test_level})")
export KNAPSACK_TEST_FILE_PATTERN="${pattern}"
fi
diff --git a/spec/factories/namespaces/project_namespaces.rb b/spec/factories/namespaces/project_namespaces.rb
index 10b86f48090..ca9fc5f8768 100644
--- a/spec/factories/namespaces/project_namespaces.rb
+++ b/spec/factories/namespaces/project_namespaces.rb
@@ -3,10 +3,11 @@
FactoryBot.define do
factory :project_namespace, class: 'Namespaces::ProjectNamespace' do
project
+ parent { project.namespace }
+ visibility_level { project.visibility_level }
name { project.name }
path { project.path }
type { Namespaces::ProjectNamespace.sti_name }
owner { nil }
- parent factory: :group
end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 9413fae02e0..34e2ca7c8a7 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -226,6 +226,7 @@ RSpec.describe 'Environments page', :js do
end
it 'does not show terminal button' do
+ expect(page).not_to have_button(_('More actions'))
expect(page).not_to have_terminal_button
end
@@ -273,6 +274,7 @@ RSpec.describe 'Environments page', :js do
let(:role) { :maintainer }
it 'shows the terminal button' do
+ click_button(_('More actions'))
expect(page).to have_terminal_button
end
end
@@ -281,6 +283,7 @@ RSpec.describe 'Environments page', :js do
let(:role) { :developer }
it 'does not show terminal button' do
+ expect(page).not_to have_button(_('More actions'))
expect(page).not_to have_terminal_button
end
end
@@ -515,7 +518,7 @@ RSpec.describe 'Environments page', :js do
end
def have_terminal_button
- have_link(nil, href: terminal_project_environment_path(project, environment))
+ have_link(_('Terminal'), href: terminal_project_environment_path(project, environment))
end
def visit_environments(project, **opts)
diff --git a/spec/frontend/environments/environment_delete_spec.js b/spec/frontend/environments/environment_delete_spec.js
index a8c288a3bd8..2d8cff0c74a 100644
--- a/spec/frontend/environments/environment_delete_spec.js
+++ b/spec/frontend/environments/environment_delete_spec.js
@@ -1,4 +1,4 @@
-import { GlButton } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import DeleteComponent from '~/environments/components/environment_delete.vue';
@@ -15,7 +15,7 @@ describe('External URL Component', () => {
});
};
- const findButton = () => wrapper.find(GlButton);
+ const findDropdownItem = () => wrapper.find(GlDropdownItem);
beforeEach(() => {
jest.spyOn(window, 'confirm');
@@ -23,14 +23,15 @@ describe('External URL Component', () => {
createWrapper();
});
- it('should render a button to delete the environment', () => {
- expect(findButton().exists()).toBe(true);
- expect(wrapper.attributes('title')).toEqual('Delete environment');
+ it('should render a dropdown item to delete the environment', () => {
+ expect(findDropdownItem().exists()).toBe(true);
+ expect(wrapper.text()).toEqual('Delete environment');
+ expect(findDropdownItem().attributes('variant')).toBe('danger');
});
it('emits requestDeleteEnvironment in the event hub when button is clicked', () => {
jest.spyOn(eventHub, '$emit');
- findButton().vm.$emit('click');
+ findDropdownItem().vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('requestDeleteEnvironment', wrapper.vm.environment);
});
});
diff --git a/spec/frontend/environments/environment_monitoring_spec.js b/spec/frontend/environments/environment_monitoring_spec.js
index 3a53b57c3c6..98dd9edd812 100644
--- a/spec/frontend/environments/environment_monitoring_spec.js
+++ b/spec/frontend/environments/environment_monitoring_spec.js
@@ -1,6 +1,6 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import MonitoringComponent from '~/environments/components/environment_monitoring.vue';
+import { __ } from '~/locale';
describe('Monitoring Component', () => {
let wrapper;
@@ -8,31 +8,19 @@ describe('Monitoring Component', () => {
const monitoringUrl = 'https://gitlab.com';
const createWrapper = () => {
- wrapper = shallowMount(MonitoringComponent, {
+ wrapper = mountExtended(MonitoringComponent, {
propsData: {
monitoringUrl,
},
});
};
- const findButtons = () => wrapper.findAll(GlButton);
- const findButtonsByIcon = (icon) =>
- findButtons().filter((button) => button.props('icon') === icon);
-
beforeEach(() => {
createWrapper();
});
- describe('computed', () => {
- it('title', () => {
- expect(wrapper.vm.title).toBe('Monitoring');
- });
- });
-
it('should render a link to environment monitoring page', () => {
- expect(wrapper.attributes('href')).toEqual(monitoringUrl);
- expect(findButtonsByIcon('chart').length).toBe(1);
- expect(wrapper.attributes('title')).toBe('Monitoring');
- expect(wrapper.attributes('aria-label')).toBe('Monitoring');
+ const link = wrapper.findByRole('menuitem', { name: __('Monitoring') });
+ expect(link.attributes('href')).toEqual(monitoringUrl);
});
});
diff --git a/spec/frontend/environments/environment_pin_spec.js b/spec/frontend/environments/environment_pin_spec.js
index 5cdd52294b6..a9a58071e12 100644
--- a/spec/frontend/environments/environment_pin_spec.js
+++ b/spec/frontend/environments/environment_pin_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PinComponent from '~/environments/components/environment_pin.vue';
import eventHub from '~/environments/event_hub';
@@ -30,15 +30,15 @@ describe('Pin Component', () => {
wrapper.destroy();
});
- it('should render the component with thumbtack icon', () => {
- expect(wrapper.find(GlIcon).props('name')).toBe('thumbtack');
+ it('should render the component with descriptive text', () => {
+ expect(wrapper.text()).toBe('Prevent auto-stopping');
});
it('should emit onPinClick when clicked', () => {
const eventHubSpy = jest.spyOn(eventHub, '$emit');
- const button = wrapper.find(GlButton);
+ const item = wrapper.find(GlDropdownItem);
- button.vm.$emit('click');
+ item.vm.$emit('click');
expect(eventHubSpy).toHaveBeenCalledWith('cancelAutoStop', autoStopUrl);
});
diff --git a/spec/frontend/environments/environment_rollback_spec.js b/spec/frontend/environments/environment_rollback_spec.js
index b6c3d436c18..cde675cd9e7 100644
--- a/spec/frontend/environments/environment_rollback_spec.js
+++ b/spec/frontend/environments/environment_rollback_spec.js
@@ -1,5 +1,5 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import RollbackComponent from '~/environments/components/environment_rollback.vue';
import eventHub from '~/environments/event_hub';
@@ -7,7 +7,7 @@ describe('Rollback Component', () => {
const retryUrl = 'https://gitlab.com/retry';
it('Should render Re-deploy label when isLastDeployment is true', () => {
- const wrapper = mount(RollbackComponent, {
+ const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: true,
@@ -15,11 +15,11 @@ describe('Rollback Component', () => {
},
});
- expect(wrapper.element).toHaveSpriteIcon('repeat');
+ expect(wrapper.text()).toBe('Re-deploy to environment');
});
it('Should render Rollback label when isLastDeployment is false', () => {
- const wrapper = mount(RollbackComponent, {
+ const wrapper = shallowMount(RollbackComponent, {
propsData: {
retryUrl,
isLastDeployment: false,
@@ -27,7 +27,7 @@ describe('Rollback Component', () => {
},
});
- expect(wrapper.element).toHaveSpriteIcon('redo');
+ expect(wrapper.text()).toBe('Rollback environment');
});
it('should emit a "rollback" event on button click', () => {
@@ -40,7 +40,7 @@ describe('Rollback Component', () => {
},
},
});
- const button = wrapper.find(GlButton);
+ const button = wrapper.find(GlDropdownItem);
button.vm.$emit('click');
diff --git a/spec/frontend/environments/environment_terminal_button_spec.js b/spec/frontend/environments/environment_terminal_button_spec.js
index 2475785a927..ab9f370595f 100644
--- a/spec/frontend/environments/environment_terminal_button_spec.js
+++ b/spec/frontend/environments/environment_terminal_button_spec.js
@@ -1,12 +1,13 @@
-import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import TerminalComponent from '~/environments/components/environment_terminal_button.vue';
+import { __ } from '~/locale';
-describe('Stop Component', () => {
+describe('Terminal Component', () => {
let wrapper;
const terminalPath = '/path';
const mountWithProps = (props) => {
- wrapper = shallowMount(TerminalComponent, {
+ wrapper = mountExtended(TerminalComponent, {
propsData: props,
});
};
@@ -15,17 +16,9 @@ describe('Stop Component', () => {
mountWithProps({ terminalPath });
});
- describe('computed', () => {
- it('title', () => {
- expect(wrapper.vm.title).toEqual('Terminal');
- });
- });
-
it('should render a link to open a web terminal with the provided path', () => {
- expect(wrapper.element.tagName).toBe('A');
- expect(wrapper.attributes('title')).toBe('Terminal');
- expect(wrapper.attributes('aria-label')).toBe('Terminal');
- expect(wrapper.attributes('href')).toBe(terminalPath);
+ const link = wrapper.findByRole('menuitem', { name: __('Terminal') });
+ expect(link.attributes('href')).toBe(terminalPath);
});
it('should render a non-disabled button', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
index 7c653028956..1b556be5873 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/app_spec.js.snap
@@ -1,10 +1,12 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`packages_list_app renders 1`] = `
+exports[`PackagesListApp renders 1`] = `
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
index 0187f7e410f..3958cdf21bb 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/app_spec.js
@@ -1,15 +1,40 @@
import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { createLocalVue } from '@vue/test-utils';
+
+import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import PackageListApp from '~/packages_and_registries/package_registry/components/list/app.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
+import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
-import * as packageUtils from '~/packages_and_registries/shared/utils';
+import {
+ PROJECT_RESOURCE_TYPE,
+ GROUP_RESOURCE_TYPE,
+ LIST_QUERY_DEBOUNCE_TIME,
+} from '~/packages_and_registries/package_registry/constants';
+
+import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
+
+import { packagesListQuery } from '../../mock_data';
jest.mock('~/lib/utils/common_utils');
jest.mock('~/flash');
-describe('packages_list_app', () => {
+const localVue = createLocalVue();
+
+describe('PackagesListApp', () => {
let wrapper;
+ let apolloProvider;
+
+ const defaultProvide = {
+ packageHelpUrl: 'packageHelpUrl',
+ emptyListIllustration: 'emptyListIllustration',
+ emptyListHelpUrl: 'emptyListHelpUrl',
+ isGroupPage: true,
+ fullPath: 'gitlab-org',
+ };
const PackageList = {
name: 'package-list',
@@ -18,9 +43,21 @@ describe('packages_list_app', () => {
const GlLoadingIcon = { name: 'gl-loading-icon', template: 'loading
' };
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
+ const findSearch = () => wrapper.findComponent(PackageSearch);
+
+ const mountComponent = ({
+ resolver = jest.fn().mockResolvedValue(packagesListQuery()),
+ provide = defaultProvide,
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[getPackagesQuery, resolver]];
+ apolloProvider = createMockApollo(requestHandlers);
- const mountComponent = () => {
wrapper = shallowMountExtended(PackageListApp, {
+ localVue,
+ apolloProvider,
+ provide,
stubs: {
GlEmptyState,
GlLoadingIcon,
@@ -28,30 +65,90 @@ describe('packages_list_app', () => {
GlSprintf,
GlLink,
},
- provide: {
- packageHelpUrl: 'packageHelpUrl',
- emptyListIllustration: 'emptyListIllustration',
- emptyListHelpUrl: 'emptyListHelpUrl',
- },
});
};
- beforeEach(() => {
- jest.spyOn(packageUtils, 'getQueryParams').mockReturnValue({});
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('renders', () => {
+ const waitForDebouncedApollo = () => {
+ jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+ return waitForPromises();
+ };
+
+ it('renders', async () => {
mountComponent();
+
+ await waitForDebouncedApollo();
+
expect(wrapper.element).toMatchSnapshot();
});
- it('has a package title', () => {
+ it('has a package title', async () => {
mountComponent();
+ await waitForDebouncedApollo();
+
expect(findPackageTitle().exists()).toBe(true);
+ expect(findPackageTitle().props('count')).toBe(2);
+ });
+
+ describe('search component', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findSearch().exists()).toBe(true);
+ });
+
+ it('on update triggers a new query with updated values', async () => {
+ const resolver = jest.fn().mockResolvedValue(packagesListQuery());
+ mountComponent({ resolver });
+
+ const payload = {
+ sort: 'VERSION_DESC',
+ filters: { packageName: 'foo', packageType: 'CONAN' },
+ };
+
+ findSearch().vm.$emit('update', payload);
+
+ await waitForDebouncedApollo();
+ jest.advanceTimersByTime(LIST_QUERY_DEBOUNCE_TIME);
+
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({
+ groupSort: payload.sort,
+ ...payload.filters,
+ }),
+ );
+ });
+ });
+
+ describe.each`
+ type | sortType
+ ${PROJECT_RESOURCE_TYPE} | ${'sort'}
+ ${GROUP_RESOURCE_TYPE} | ${'groupSort'}
+ `('$type query', ({ type, sortType }) => {
+ let provide;
+ let resolver;
+
+ const isGroupPage = type === GROUP_RESOURCE_TYPE;
+
+ beforeEach(() => {
+ provide = { ...defaultProvide, isGroupPage };
+ resolver = jest.fn().mockResolvedValue(packagesListQuery(type));
+ mountComponent({ provide, resolver });
+ return waitForDebouncedApollo();
+ });
+
+ it('succeeds', () => {
+ expect(findPackageTitle().props('count')).toBe(2);
+ });
+
+ it('calls the resolver with the right parameters', () => {
+ expect(resolver).toHaveBeenCalledWith(
+ expect.objectContaining({ isGroupPage, [sortType]: '' }),
+ );
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
index 42bc9fa3a9e..e65b2a6f320 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_search_spec.js
@@ -1,113 +1,61 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { sortableFields } from '~/packages/list/utils';
import component from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import PackageTypeToken from '~/packages_and_registries/package_registry/components/list/tokens/package_type_token.vue';
import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { getQueryParams, extractFilterAndSorting } from '~/packages_and_registries/shared/utils';
-const localVue = createLocalVue();
-localVue.use(Vuex);
+jest.mock('~/packages_and_registries/shared/utils');
+
+useMockLocationHelper();
describe('Package Search', () => {
let wrapper;
- let store;
+
+ const defaultQueryParamsMock = {
+ filters: ['foo'],
+ sorting: { sort: 'desc' },
+ };
const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
const findUrlSync = () => wrapper.findComponent(UrlSync);
- const createStore = (isGroupPage) => {
- const state = {
- config: {
- isGroupPage,
- },
- sorting: {
- orderBy: 'version',
- sort: 'desc',
- },
- filter: [],
- };
- store = new Vuex.Store({
- state,
- });
- store.dispatch = jest.fn();
- };
-
const mountComponent = (isGroupPage = false) => {
- createStore(isGroupPage);
-
- wrapper = shallowMount(component, {
- localVue,
- store,
+ wrapper = shallowMountExtended(component, {
+ provide() {
+ return {
+ isGroupPage,
+ };
+ },
stubs: {
UrlSync,
},
});
};
+ beforeEach(() => {
+ extractFilterAndSorting.mockReturnValue(defaultQueryParamsMock);
+ });
+
afterEach(() => {
wrapper.destroy();
- wrapper = null;
});
- it('has a registry search component', () => {
+ it('has a registry search component', async () => {
mountComponent();
+ await nextTick();
+
expect(findRegistrySearch().exists()).toBe(true);
- expect(findRegistrySearch().props()).toMatchObject({
- filter: store.state.filter,
- sorting: store.state.sorting,
- tokens: expect.arrayContaining([
- expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
- ]),
- sortableFields: sortableFields(),
- });
});
- it.each`
- isGroupPage | page
- ${false} | ${'project'}
- ${true} | ${'group'}
- `('in a $page page binds the right props', ({ isGroupPage }) => {
- mountComponent(isGroupPage);
-
- expect(findRegistrySearch().props()).toMatchObject({
- filter: store.state.filter,
- sorting: store.state.sorting,
- tokens: expect.arrayContaining([
- expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
- ]),
- sortableFields: sortableFields(isGroupPage),
- });
- });
-
- it('on sorting:changed emits update event and calls vuex setSorting', () => {
- const payload = { sort: 'foo' };
-
+ it('registry search is mounted after mount', async () => {
mountComponent();
- findRegistrySearch().vm.$emit('sorting:changed', payload);
-
- expect(store.dispatch).toHaveBeenCalledWith('setSorting', payload);
- expect(wrapper.emitted('update')).toEqual([[]]);
- });
-
- it('on filter:changed calls vuex setFilter', () => {
- const payload = ['foo'];
-
- mountComponent();
-
- findRegistrySearch().vm.$emit('filter:changed', payload);
-
- expect(store.dispatch).toHaveBeenCalledWith('setFilter', payload);
- });
-
- it('on filter:submit emits update event', () => {
- mountComponent();
-
- findRegistrySearch().vm.$emit('filter:submit');
-
- expect(wrapper.emitted('update')).toEqual([[]]);
+ expect(findRegistrySearch().exists()).toBe(false);
});
it('has a UrlSync component', () => {
@@ -116,13 +64,102 @@ describe('Package Search', () => {
expect(findUrlSync().exists()).toBe(true);
});
- it('on query:changed calls updateQuery from UrlSync', () => {
+ it.each`
+ isGroupPage | page
+ ${false} | ${'project'}
+ ${true} | ${'group'}
+ `('in a $page page binds the right props', async ({ isGroupPage }) => {
+ mountComponent(isGroupPage);
+
+ await nextTick();
+
+ expect(findRegistrySearch().props()).toMatchObject({
+ tokens: expect.arrayContaining([
+ expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ ]),
+ sortableFields: sortableFields(isGroupPage),
+ });
+ });
+
+ it('on sorting:changed emits update event and update internal sort', async () => {
+ const payload = { sort: 'foo' };
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('sorting:changed', payload);
+
+ await nextTick();
+
+ expect(findRegistrySearch().props('sorting')).toEqual({ sort: 'foo', orderBy: 'name' });
+
+ // there is always a first call on mounted that emits up default values
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: {
+ packageName: '',
+ packageType: undefined,
+ },
+ sort: 'NAME_FOO',
+ },
+ ]);
+ });
+
+ it('on filter:changed updates the filters', async () => {
+ const payload = ['foo'];
+
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('filter:changed', payload);
+
+ await nextTick();
+
+ expect(findRegistrySearch().props('filter')).toEqual(['foo']);
+ });
+
+ it('on filter:submit emits update event', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(wrapper.emitted('update')[1]).toEqual([
+ {
+ filters: {
+ packageName: '',
+ packageType: undefined,
+ },
+ sort: 'NAME_DESC',
+ },
+ ]);
+ });
+
+ it('on query:changed calls updateQuery from UrlSync', async () => {
jest.spyOn(UrlSync.methods, 'updateQuery').mockImplementation(() => {});
mountComponent();
+ await nextTick();
+
findRegistrySearch().vm.$emit('query:changed');
expect(UrlSync.methods.updateQuery).toHaveBeenCalled();
});
+
+ it('sets the component sorting and filtering based on the querystring', async () => {
+ mountComponent();
+
+ await nextTick();
+
+ expect(getQueryParams).toHaveBeenCalled();
+
+ expect(findRegistrySearch().props()).toMatchObject({
+ filter: defaultQueryParamsMock.filters,
+ sorting: defaultQueryParamsMock.sorting,
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 9438a2d2d72..8b1ac377531 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -249,3 +249,27 @@ export const packageDestroyFileMutationError = () => ({
},
],
});
+
+export const packagesListQuery = (type = 'group') => ({
+ data: {
+ [type]: {
+ packages: {
+ count: 2,
+ nodes: [
+ {
+ __typename: 'Package',
+ id: 'gid://gitlab/Packages::Package/247',
+ name: 'version_test1',
+ },
+ {
+ __typename: 'Package',
+ id: 'gid://gitlab/Packages::Package/246',
+ name: 'version_test1',
+ },
+ ],
+ __typename: 'PackageConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+});
diff --git a/spec/graphql/resolvers/issues_resolver_spec.rb b/spec/graphql/resolvers/issues_resolver_spec.rb
index 3a7c778da62..9897e697009 100644
--- a/spec/graphql/resolvers/issues_resolver_spec.rb
+++ b/spec/graphql/resolvers/issues_resolver_spec.rb
@@ -26,7 +26,14 @@ RSpec.describe Resolvers::IssuesResolver do
expect(described_class).to have_nullable_graphql_type(Types::IssueType.connection_type)
end
+ shared_context 'filtering for confidential issues' do
+ let_it_be(:confidential_issue1) { create(:issue, project: project, confidential: true) }
+ let_it_be(:confidential_issue2) { create(:issue, project: other_project, confidential: true) }
+ end
+
context "with a project" do
+ let(:obj) { project }
+
before_all do
project.add_developer(current_user)
project.add_reporter(reporter)
@@ -222,6 +229,42 @@ RSpec.describe Resolvers::IssuesResolver do
end
end
+ context 'confidential issues' do
+ include_context 'filtering for confidential issues'
+
+ context "when user is allowed to view confidential issues" do
+ it 'returns all viewable issues by default' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2, confidential_issue1)
+ end
+
+ it 'returns only the non-confidential issues for the project when filter is set to false' do
+ expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2)
+ end
+
+ it "returns only the confidential issues for the project when filter is set to true" do
+ expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1)
+ end
+ end
+
+ context "when user is not allowed to see confidential issues" do
+ before do
+ project.add_guest(current_user)
+ end
+
+ it 'returns all viewable issues by default' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2)
+ end
+
+ it 'does not return the confidential issues when filter is set to false' do
+ expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2)
+ end
+
+ it 'does not return the confidential issues when filter is set to true' do
+ expect(resolve_issues({ confidential: true })).to be_empty
+ end
+ end
+ end
+
context 'when searching issues' do
it 'returns correct issues' do
expect(resolve_issues(search: 'foo')).to contain_exactly(issue2)
@@ -519,32 +562,72 @@ RSpec.describe Resolvers::IssuesResolver do
end
context "with a group" do
+ let(:obj) { group }
+
before do
group.add_developer(current_user)
end
describe '#resolve' do
it 'finds all group issues' do
- result = resolve(described_class, obj: group, ctx: { current_user: current_user })
-
- expect(result).to contain_exactly(issue1, issue2, issue3)
+ expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
end
it 'returns issues without the specified issue_type' do
- result = resolve(described_class, obj: group, ctx: { current_user: current_user }, args: { not: { types: ['issue'] } })
+ expect(resolve_issues({ not: { types: ['issue'] } })).to contain_exactly(issue1)
+ end
- expect(result).to contain_exactly(issue1)
+ context "confidential issues" do
+ include_context 'filtering for confidential issues'
+
+ context "when user is allowed to view confidential issues" do
+ it 'returns all viewable issues by default' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2, issue3, confidential_issue1, confidential_issue2)
+ end
+
+ context 'filtering for confidential issues' do
+ it 'returns only the non-confidential issues for the group when filter is set to false' do
+ expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ it "returns only the confidential issues for the group when filter is set to true" do
+ expect(resolve_issues({ confidential: true })).to contain_exactly(confidential_issue1, confidential_issue2)
+ end
+ end
+ end
+
+ context "when user is not allowed to see confidential issues" do
+ before do
+ group.add_guest(current_user)
+ end
+
+ it 'returns all viewable issues by default' do
+ expect(resolve_issues).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ context 'filtering for confidential issues' do
+ it 'does not return the confidential issues when filter is set to false' do
+ expect(resolve_issues({ confidential: false })).to contain_exactly(issue1, issue2, issue3)
+ end
+
+ it 'does not return the confidential issues when filter is set to true' do
+ expect(resolve_issues({ confidential: true })).to be_empty
+ end
+ end
+ end
end
end
end
context "when passing a non existent, batch loaded project" do
- let(:project) do
+ let!(:project) do
BatchLoader::GraphQL.for("non-existent-path").batch do |_fake_paths, loader, _|
loader.call("non-existent-path", nil)
end
end
+ let(:obj) { project }
+
it "returns nil without breaking" do
expect(resolve_issues(iids: ["don't", "break"])).to be_empty
end
@@ -565,6 +648,6 @@ RSpec.describe Resolvers::IssuesResolver do
end
def resolve_issues(args = {}, context = { current_user: current_user })
- resolve(described_class, obj: project, args: args, ctx: context)
+ resolve(described_class, obj: obj, args: args, ctx: context)
end
end
diff --git a/spec/helpers/packages_helper_spec.rb b/spec/helpers/packages_helper_spec.rb
index 06c6cccd488..2af572850da 100644
--- a/spec/helpers/packages_helper_spec.rb
+++ b/spec/helpers/packages_helper_spec.rb
@@ -260,4 +260,34 @@ RSpec.describe PackagesHelper do
end
end
end
+
+ describe '#packages_list_data' do
+ let_it_be(:resource) { project }
+ let_it_be(:type) { 'project' }
+
+ let(:expected_result) do
+ {
+ resource_id: resource.id,
+ full_path: resource.full_path,
+ page_type: type
+ }
+ end
+
+ subject(:result) { helper.packages_list_data(type, resource) }
+
+ context 'at a project level' do
+ it 'populates presenter data' do
+ expect(result).to match(hash_including(expected_result))
+ end
+ end
+
+ context 'at a group level' do
+ let_it_be(:resource) { create(:group) }
+ let_it_be(:type) { 'group' }
+
+ it 'populates presenter data' do
+ expect(result).to match(hash_including(expected_result))
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
index 0c28515b574..3aa6b2e3c05 100644
--- a/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/seed/build_spec.rb
@@ -440,17 +440,30 @@ RSpec.describe Gitlab::Ci::Pipeline::Seed::Build do
context 'when the environment name is invalid' do
let(:attributes) { { name: 'deploy', ref: 'master', environment: '!!!' } }
- it_behaves_like 'non-deployment job'
- it_behaves_like 'ensures environment inexistence'
+ it 'fails the job with a failure reason and does not create an environment' do
+ expect(subject).to be_failed
+ expect(subject).to be_environment_creation_failure
+ expect(subject.metadata.expanded_environment_name).to be_nil
+ expect(Environment.exists?(name: expected_environment_name)).to eq(false)
+ end
- it 'tracks an exception' do
- expect(Gitlab::ErrorTracking).to receive(:track_exception)
- .with(an_instance_of(described_class::EnvironmentCreationFailure),
- project_id: project.id,
- reason: %q{Name can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'})
- .once
+ context 'when surface_environment_creation_failure feature flag is disabled' do
+ before do
+ stub_feature_flags(surface_environment_creation_failure: false)
+ end
- subject
+ it_behaves_like 'non-deployment job'
+ it_behaves_like 'ensures environment inexistence'
+
+ it 'tracks an exception' do
+ expect(Gitlab::ErrorTracking).to receive(:track_exception)
+ .with(an_instance_of(described_class::EnvironmentCreationFailure),
+ project_id: project.id,
+ reason: %q{Name can contain only letters, digits, '-', '_', '/', '$', '{', '}', '.', and spaces, but it cannot start or end with '/'})
+ .once
+
+ subject
+ end
end
end
end
diff --git a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
index 4784f4270b2..fb17ba0e121 100644
--- a/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
+++ b/spec/lib/gitlab/database/load_balancing/load_balancer_spec.rb
@@ -274,6 +274,14 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
expect { lb.retry_with_backoff { raise } }.to raise_error(RuntimeError)
end
+
+ it 'skips retries when only the primary is used' do
+ allow(lb).to receive(:primary_only?).and_return(true)
+
+ expect(lb).not_to receive(:sleep)
+
+ expect { lb.retry_with_backoff { raise } }.to raise_error(RuntimeError)
+ end
end
describe '#connection_error?' do
@@ -283,6 +291,12 @@ RSpec.describe Gitlab::Database::LoadBalancing::LoadBalancer, :request_store do
expect(lb.connection_error?(error)).to eq(true)
end
+ it 'returns false for a missing database error' do
+ error = ActiveRecord::NoDatabaseError.new
+
+ expect(lb.connection_error?(error)).to eq(false)
+ end
+
it 'returns true for a wrapped connection error' do
wrapped = wrapped_exception(ActiveRecord::StatementInvalid, ActiveRecord::ConnectionNotEstablished)
diff --git a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
index 4996b0a0089..222198a58ac 100644
--- a/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
+++ b/spec/lib/gitlab/usage_data_counters/ci_template_unique_counter_spec.rb
@@ -6,97 +6,62 @@ RSpec.describe Gitlab::UsageDataCounters::CiTemplateUniqueCounter do
describe '.track_unique_project_event' do
using RSpec::Parameterized::TableSyntax
- where(:template, :config_source, :expected_event) do
- # Implicit Auto DevOps usage
- 'Auto-DevOps.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops'
- 'Jobs/Build.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_build'
- 'Jobs/Deploy.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_auto_devops_deploy'
- 'Security/SAST.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_sast'
- 'Security/Secret-Detection.gitlab-ci.yml' | :auto_devops_source | 'p_ci_templates_implicit_security_secret_detection'
- # Explicit include:template usage
- '5-Minute-Production-App.gitlab-ci.yml' | :repository_source | 'p_ci_templates_5_min_production_app'
- 'Auto-DevOps.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops'
- 'AWS/CF-Provision-and-Deploy-EC2.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_cf_deploy_ec2'
- 'AWS/Deploy-ECS.gitlab-ci.yml' | :repository_source | 'p_ci_templates_aws_deploy_ecs'
- 'Jobs/Build.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_build'
- 'Jobs/Deploy.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy'
- 'Jobs/Deploy.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_auto_devops_deploy_latest'
- 'Security/SAST.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_sast'
- 'Security/Secret-Detection.gitlab-ci.yml' | :repository_source | 'p_ci_templates_security_secret_detection'
- 'Terraform/Base.latest.gitlab-ci.yml' | :repository_source | 'p_ci_templates_terraform_base_latest'
- end
+ let(:project_id) { 1 }
- with_them do
- it_behaves_like 'tracking unique hll events' do
- subject(:request) { described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source) }
+ shared_examples 'tracks template' do
+ it "has an event defined for template" do
+ expect do
+ described_class.track_unique_project_event(
+ project_id: project_id,
+ template: template_path,
+ config_source: config_source
+ )
+ end.not_to raise_error
+ end
- let(:project_id) { 1 }
- let(:target_id) { expected_event }
- let(:expected_type) { instance_of(Integer) }
+ it "tracks template" do
+ expanded_template_name = described_class.expand_template_name(template_path)
+ expected_template_event_name = described_class.ci_template_event_name(expanded_template_name, config_source)
+ expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(receive(:track_event)).with(expected_template_event_name, values: project_id)
+
+ described_class.track_unique_project_event(project_id: project_id, template: template_path, config_source: config_source)
end
end
- context 'known_events coverage tests' do
- let(:project_id) { 1 }
+ context 'with explicit includes' do
let(:config_source) { :repository_source }
- # These tests help guard against missing "explicit" events in known_events/ci_templates.yml
- context 'explicit include:template events' do
- described_class::TEMPLATE_TO_EVENT.keys.each do |template|
- it "does not raise error for #{template}" do
- expect do
- described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
- end.not_to raise_error
+ (described_class.ci_templates - ['Verify/Browser-Performance.latest.gitlab-ci.yml', 'Verify/Browser-Performance.gitlab-ci.yml']).each do |template|
+ context "for #{template}" do
+ let(:template_path) { template }
+
+ include_examples 'tracks template'
+ end
+ end
+ end
+
+ context 'with implicit includes' do
+ let(:config_source) { :auto_devops_source }
+
+ [
+ ['', ['Auto-DevOps.gitlab-ci.yml']],
+ ['Jobs', described_class.ci_templates('lib/gitlab/ci/templates/Jobs')],
+ ['Security', described_class.ci_templates('lib/gitlab/ci/templates/Security')]
+ ].each do |directory, templates|
+ templates.each do |template|
+ context "for #{template}" do
+ let(:template_path) { File.join(directory, template) }
+
+ include_examples 'tracks template'
end
end
end
-
- # This test is to help guard against missing "implicit" events in known_events/ci_templates.yml
- it 'does not raise error for any template in an implicit Auto DevOps pipeline' do
- project = create(:project, :auto_devops)
- pipeline = double(project: project)
- command = double
- result = Gitlab::Ci::YamlProcessor.new(
- Gitlab::Ci::Pipeline::Chain::Config::Content::AutoDevops.new(pipeline, command).content,
- project: project,
- user: double,
- sha: 'd310cc759caaa20cd05a9e0983d6017896d9c34c'
- ).execute
-
- config_source = :auto_devops_source
-
- result.included_templates.each do |template|
- expect do
- described_class.track_unique_project_event(project_id: project.id, template: template, config_source: config_source)
- end.not_to raise_error
- end
- end
end
- context 'templates outside of TEMPLATE_TO_EVENT' do
- let(:project_id) { 1 }
- let(:config_source) { :repository_source }
-
- described_class.ci_templates.each do |template|
- next if described_class::TEMPLATE_TO_EVENT.key?(template)
-
- it "has an event defined for #{template}" do
- expect do
- described_class.track_unique_project_event(
- project_id: project_id,
- template: template,
- config_source: config_source
- )
- end.not_to raise_error
- end
-
- it "tracks #{template}" do
- expected_template_event_name = described_class.ci_template_event_name(template, :repository_source)
- expect(Gitlab::UsageDataCounters::HLLRedisCounter).to(receive(:track_event)).with(expected_template_event_name, values: project_id)
-
- described_class.track_unique_project_event(project_id: project_id, template: template, config_source: config_source)
- end
- end
+ it 'expands short template names' do
+ expect do
+ described_class.track_unique_project_event(project_id: 1, template: 'Dependency-Scanning.gitlab-ci.yml', config_source: :repository_source)
+ end.not_to raise_error
end
end
end
diff --git a/spec/models/upload_spec.rb b/spec/models/upload_spec.rb
index 6bac5e31435..0ac684cd04c 100644
--- a/spec/models/upload_spec.rb
+++ b/spec/models/upload_spec.rb
@@ -242,4 +242,28 @@ RSpec.describe Upload do
it { expect(subject.uploader_context).to match(a_hash_including(secret: 'secret', identifier: 'file.txt')) }
end
+
+ describe '#update_project_statistics' do
+ let_it_be(:project) { create(:project) }
+
+ subject do
+ create(:upload, model: project)
+ end
+
+ it 'updates project statistics when upload is added' do
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(project.id, [], [:uploads_size])
+
+ subject.save!
+ end
+
+ it 'updates project statistics when upload is removed' do
+ subject.save!
+
+ expect(ProjectCacheWorker).to receive(:perform_async)
+ .with(project.id, [], [:uploads_size])
+
+ subject.destroy!
+ end
+ end
end
diff --git a/spec/presenters/commit_status_presenter_spec.rb b/spec/presenters/commit_status_presenter_spec.rb
index 4b2441d656e..f0bf1b860e4 100644
--- a/spec/presenters/commit_status_presenter_spec.rb
+++ b/spec/presenters/commit_status_presenter_spec.rb
@@ -15,6 +15,25 @@ RSpec.describe CommitStatusPresenter do
expect(described_class.superclass).to eq(Gitlab::View::Presenter::Delegated)
end
+ describe '#callout_failure_message' do
+ subject { presenter.callout_failure_message }
+
+ context 'when troubleshooting doc is available' do
+ let(:failure_reason) { :environment_creation_failure }
+
+ before do
+ build.failure_reason = failure_reason
+ end
+
+ it 'appends the troubleshooting link' do
+ doc = described_class::TROUBLESHOOTING_DOC[failure_reason]
+
+ expect(subject).to eq("#{described_class.callout_failure_messages[failure_reason]} " \
+ "How do I fix it?")
+ end
+ end
+ end
+
describe 'covers all failure reasons' do
let(:message) { presenter.callout_failure_message }
diff --git a/spec/requests/api/graphql/group/issues_spec.rb b/spec/requests/api/graphql/group/issues_spec.rb
new file mode 100644
index 00000000000..332bf242e9c
--- /dev/null
+++ b/spec/requests/api/graphql/group/issues_spec.rb
@@ -0,0 +1,123 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'getting an issue list for a group' do
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+ let_it_be(:group1) { create(:group) }
+ let_it_be(:group2) { create(:group) }
+ let_it_be(:project1) { create(:project, :public, group: group1) }
+ let_it_be(:project2) { create(:project, :private, group: group1) }
+ let_it_be(:project3) { create(:project, :public, group: group2) }
+ let_it_be(:issue1) { create(:issue, project: project1) }
+ let_it_be(:issue2) { create(:issue, project: project2) }
+ let_it_be(:issue3) { create(:issue, project: project3) }
+
+ let(:issue1_gid) { issue1.to_global_id.to_s }
+ let(:issue2_gid) { issue2.to_global_id.to_s }
+ let(:issues_data) { graphql_data['group']['issues']['edges'] }
+ let(:issue_filter_params) { {} }
+
+ let(:fields) do
+ <<~QUERY
+ edges {
+ node {
+ #{all_graphql_fields_for('issues'.classify)}
+ }
+ }
+ QUERY
+ end
+
+ let(:query) do
+ graphql_query_for(
+ 'group',
+ { 'fullPath' => group1.full_path },
+ query_graphql_field('issues', issue_filter_params, fields)
+ )
+ end
+
+ it_behaves_like 'a working graphql query' do
+ before do
+ post_graphql(query, current_user: current_user)
+ end
+ end
+
+ context 'when there is a confidential issue' do
+ let_it_be(:confidential_issue1) { create(:issue, :confidential, project: project1) }
+ let_it_be(:confidential_issue2) { create(:issue, :confidential, project: project2) }
+ let_it_be(:confidential_issue3) { create(:issue, :confidential, project: project3) }
+
+ let(:confidential_issue1_gid) { confidential_issue1.to_global_id.to_s }
+ let(:confidential_issue2_gid) { confidential_issue2.to_global_id.to_s }
+
+ context 'when the user cannot see confidential issues' do
+ before do
+ group1.add_guest(current_user)
+ end
+
+ it 'returns issues without confidential issues for the group' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
+ end
+
+ context 'filtering for confidential issues' do
+ let(:issue_filter_params) { { confidential: true } }
+
+ it 'returns no issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to be_empty
+ end
+ end
+
+ context 'filtering for non-confidential issues' do
+ let(:issue_filter_params) { { confidential: false } }
+
+ it 'returns correctly filtered issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
+ end
+ end
+ end
+
+ context 'when the user can see confidential issues' do
+ before do
+ group1.add_developer(current_user)
+ end
+
+ it 'returns issues with confidential issues for the group' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid, confidential_issue1_gid, confidential_issue2_gid)
+ end
+
+ context 'filtering for confidential issues' do
+ let(:issue_filter_params) { { confidential: true } }
+
+ it 'returns correctly filtered issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(confidential_issue1_gid, confidential_issue2_gid)
+ end
+ end
+
+ context 'filtering for non-confidential issues' do
+ let(:issue_filter_params) { { confidential: false } }
+
+ it 'returns correctly filtered issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(issue1_gid, issue2_gid)
+ end
+ end
+ end
+ end
+
+ def issues_ids
+ graphql_dig_at(issues_data, :node, :id)
+ end
+end
diff --git a/spec/requests/api/graphql/project/issues_spec.rb b/spec/requests/api/graphql/project/issues_spec.rb
index 0ac6d0fbc56..a62ce12a3b0 100644
--- a/spec/requests/api/graphql/project/issues_spec.rb
+++ b/spec/requests/api/graphql/project/issues_spec.rb
@@ -11,6 +11,8 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:issue_b, reload: true) { create(:issue, :with_alert, project: project) }
let_it_be(:issues, reload: true) { [issue_a, issue_b] }
+ let(:issue_a_gid) { issue_a.to_global_id.to_s }
+ let(:issue_b_gid) { issue_b.to_global_id.to_s }
let(:issues_data) { graphql_data['project']['issues']['edges'] }
let(:issue_filter_params) { {} }
@@ -66,9 +68,6 @@ RSpec.describe 'getting an issue list for a project' do
let_it_be(:upvote_award) { create(:award_emoji, :upvote, user: current_user, awardable: issue_a) }
- let(:issue_a_gid) { issue_a.to_global_id.to_s }
- let(:issue_b_gid) { issue_b.to_global_id.to_s }
-
where(:value, :gids) do
'thumbsup' | lazy { [issue_a_gid] }
'ANY' | lazy { [issue_a_gid] }
@@ -84,7 +83,7 @@ RSpec.describe 'getting an issue list for a project' do
it 'returns correctly filtered issues' do
post_graphql(query, current_user: current_user)
- expect(graphql_dig_at(issues_data, :node, :id)).to eq(gids)
+ expect(issues_ids).to eq(gids)
end
end
end
@@ -149,6 +148,8 @@ RSpec.describe 'getting an issue list for a project' do
create(:issue, :confidential, project: project)
end
+ let(:confidential_issue_gid) { confidential_issue.to_global_id.to_s }
+
context 'when the user cannot see confidential issues' do
it 'returns issues without confidential issues' do
post_graphql(query, current_user: current_user)
@@ -159,12 +160,34 @@ RSpec.describe 'getting an issue list for a project' do
expect(issue.dig('node', 'confidential')).to eq(false)
end
end
+
+ context 'filtering for confidential issues' do
+ let(:issue_filter_params) { { confidential: true } }
+
+ it 'returns no issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_data.size).to eq(0)
+ end
+ end
+
+ context 'filtering for non-confidential issues' do
+ let(:issue_filter_params) { { confidential: false } }
+
+ it 'returns correctly filtered issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
+ end
+ end
end
context 'when the user can see confidential issues' do
- it 'returns issues with confidential issues' do
+ before do
project.add_developer(current_user)
+ end
+ it 'returns issues with confidential issues' do
post_graphql(query, current_user: current_user)
expect(issues_data.size).to eq(3)
@@ -175,6 +198,26 @@ RSpec.describe 'getting an issue list for a project' do
expect(confidentials).to eq([true, false, false])
end
+
+ context 'filtering for confidential issues' do
+ let(:issue_filter_params) { { confidential: true } }
+
+ it 'returns correctly filtered issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(confidential_issue_gid)
+ end
+ end
+
+ context 'filtering for non-confidential issues' do
+ let(:issue_filter_params) { { confidential: false } }
+
+ it 'returns correctly filtered issues' do
+ post_graphql(query, current_user: current_user)
+
+ expect(issues_ids).to contain_exactly(issue_a_gid, issue_b_gid)
+ end
+ end
end
end
@@ -526,4 +569,8 @@ RSpec.describe 'getting an issue list for a project' do
include_examples 'N+1 query check'
end
end
+
+ def issues_ids
+ graphql_dig_at(issues_data, :node, :id)
+ end
end
diff --git a/spec/services/groups/transfer_service_spec.rb b/spec/services/groups/transfer_service_spec.rb
index e7928c9c2d5..bcd9573ebeb 100644
--- a/spec/services/groups/transfer_service_spec.rb
+++ b/spec/services/groups/transfer_service_spec.rb
@@ -3,6 +3,19 @@
require 'spec_helper'
RSpec.describe Groups::TransferService do
+ shared_examples 'project namespace path is in sync with project path' do
+ it 'keeps project and project namespace attributes in sync' do
+ projects_with_project_namespace.each do |project|
+ project.reload
+
+ expect(project.full_path).to eq("#{group_full_path}/#{project.path}")
+ expect(project.project_namespace.full_path).to eq(project.full_path)
+ expect(project.project_namespace.parent).to eq(project.namespace)
+ expect(project.project_namespace.visibility_level).to eq(project.visibility_level)
+ end
+ end
+ end
+
let_it_be(:user) { create(:user) }
let_it_be(:new_parent_group) { create(:group, :public) }
@@ -169,6 +182,18 @@ RSpec.describe Groups::TransferService do
expect(project.full_path).to eq("#{group.path}/#{project.path}")
end
end
+
+ context 'when projects have project namespaces' do
+ let_it_be(:project1) { create(:project, :private, namespace: group) }
+ let_it_be(:project_namespace1) { create(:project_namespace, project: project1) }
+ let_it_be(:project2) { create(:project, :private, namespace: group) }
+ let_it_be(:project_namespace2) { create(:project_namespace, project: project2) }
+
+ it_behaves_like 'project namespace path is in sync with project path' do
+ let(:group_full_path) { "#{group.path}" }
+ let(:projects_with_project_namespace) { [project1, project2] }
+ end
+ end
end
end
@@ -222,10 +247,10 @@ RSpec.describe Groups::TransferService do
context 'when the parent group has a project with the same path' do
let_it_be_with_reload(:group) { create(:group, :public, :nested, path: 'foo') }
+ let_it_be(:membership) { create(:group_member, :owner, group: new_parent_group, user: user) }
+ let_it_be(:project) { create(:project, path: 'foo', namespace: new_parent_group) }
before do
- create(:group_member, :owner, group: new_parent_group, user: user)
- create(:project, path: 'foo', namespace: new_parent_group)
group.update_attribute(:path, 'foo')
end
@@ -237,6 +262,19 @@ RSpec.describe Groups::TransferService do
transfer_service.execute(new_parent_group)
expect(transfer_service.error).to eq('Transfer failed: Validation failed: Group URL has already been taken')
end
+
+ context 'when projects have project namespaces' do
+ let!(:project_namespace) { create(:project_namespace, project: project) }
+
+ before do
+ transfer_service.execute(new_parent_group)
+ end
+
+ it_behaves_like 'project namespace path is in sync with project path' do
+ let(:group_full_path) { "#{new_parent_group.full_path}" }
+ let(:projects_with_project_namespace) { [project] }
+ end
+ end
end
context 'when the group is allowed to be transferred' do
@@ -407,6 +445,8 @@ RSpec.describe Groups::TransferService do
context 'when transferring a group with project descendants' do
let!(:project1) { create(:project, :repository, :private, namespace: group) }
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
+ let!(:project_namespace1) { create(:project_namespace, project: project1) }
+ let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
@@ -432,18 +472,30 @@ RSpec.describe Groups::TransferService do
expect(project1.private?).to be_truthy
expect(project2.internal?).to be_truthy
end
+
+ it_behaves_like 'project namespace path is in sync with project path' do
+ let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" }
+ let(:projects_with_project_namespace) { [project1, project2] }
+ end
end
context 'when the new parent has a lower visibility than the projects' do
let!(:project1) { create(:project, :repository, :public, namespace: group) }
let!(:project2) { create(:project, :repository, :public, namespace: group) }
- let(:new_parent_group) { create(:group, :private) }
+ let!(:new_parent_group) { create(:group, :private) }
+ let!(:project_namespace1) { create(:project_namespace, project: project1) }
+ let!(:project_namespace2) { create(:project_namespace, project: project2) }
it 'updates projects visibility to match the new parent' do
group.projects.each do |project|
expect(project.private?).to be_truthy
end
end
+
+ it_behaves_like 'project namespace path is in sync with project path' do
+ let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" }
+ let(:projects_with_project_namespace) { [project1, project2] }
+ end
end
end
@@ -452,6 +504,8 @@ RSpec.describe Groups::TransferService do
let!(:project2) { create(:project, :repository, :internal, namespace: group) }
let!(:subgroup1) { create(:group, :private, parent: group) }
let!(:subgroup2) { create(:group, :internal, parent: group) }
+ let!(:project_namespace1) { create(:project_namespace, project: project1) }
+ let!(:project_namespace2) { create(:project_namespace, project: project2) }
before do
TestEnv.clean_test_path
@@ -480,6 +534,11 @@ RSpec.describe Groups::TransferService do
expect(project1.redirect_routes.count).to eq(1)
expect(project2.redirect_routes.count).to eq(1)
end
+
+ it_behaves_like 'project namespace path is in sync with project path' do
+ let(:group_full_path) { "#{new_parent_group.path}/#{group.path}" }
+ let(:projects_with_project_namespace) { [project1, project2] }
+ end
end
context 'when transferring a group with nested groups and projects' do
diff --git a/spec/services/projects/transfer_service_spec.rb b/spec/services/projects/transfer_service_spec.rb
index 5a52f4fad6f..d0cb3d08317 100644
--- a/spec/services/projects/transfer_service_spec.rb
+++ b/spec/services/projects/transfer_service_spec.rb
@@ -64,6 +64,33 @@ RSpec.describe Projects::TransferService do
expect(transfer_result).to be_truthy
expect(project.namespace).to eq(group)
end
+
+ context 'when project has an associated project namespace' do
+ let!(:project_namespace) { create(:project_namespace, project: project) }
+
+ it 'keeps project namespace in sync with project' do
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to be_truthy
+
+ project_namespace_in_sync(group)
+ end
+
+ context 'when project is transferred to a deeper nested group' do
+ let(:parent_group) { create(:group) }
+ let(:sub_group) { create(:group, parent: parent_group) }
+ let(:sub_sub_group) { create(:group, parent: sub_group) }
+ let(:group) { sub_sub_group }
+
+ it 'keeps project namespace in sync with project' do
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to be_truthy
+
+ project_namespace_in_sync(sub_sub_group)
+ end
+ end
+ end
end
context 'when transfer succeeds' do
@@ -243,6 +270,16 @@ RSpec.describe Projects::TransferService do
expect(unrelated_pending_build.namespace_traversal_ids).to eq(other_project.namespace.traversal_ids)
end
end
+
+ context 'when project has an associated project namespace' do
+ let!(:project_namespace) { create(:project_namespace, project: project) }
+
+ it 'keeps project namespace in sync with project' do
+ attempt_project_transfer
+
+ project_namespace_in_sync(user.namespace)
+ end
+ end
end
context 'namespace -> no namespace' do
@@ -255,6 +292,18 @@ RSpec.describe Projects::TransferService do
expect(project.namespace).to eq(user.namespace)
expect(project.errors.messages[:new_namespace].first).to eq 'Please select a new namespace for your project.'
end
+
+ context 'when project has an associated project namespace' do
+ let!(:project_namespace) { create(:project_namespace, project: project) }
+
+ it 'keeps project namespace in sync with project' do
+ transfer_result = execute_transfer
+
+ expect(transfer_result).to be false
+
+ project_namespace_in_sync(user.namespace)
+ end
+ end
end
context 'disallow transferring of project with tags' do
@@ -655,4 +704,13 @@ RSpec.describe Projects::TransferService do
def rugged_config
rugged_repo(project.repository).config
end
+
+ def project_namespace_in_sync(group)
+ project.reload
+ expect(project.namespace).to eq(group)
+ expect(project.project_namespace.visibility_level).to eq(project.visibility_level)
+ expect(project.project_namespace.path).to eq(project.path)
+ expect(project.project_namespace.parent).to eq(project.namespace)
+ expect(project.project_namespace.traversal_ids).to eq([*project.namespace.traversal_ids, project.project_namespace.id])
+ end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index aa791d1d2e7..c8664598691 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -315,6 +315,10 @@ RSpec.configure do |config|
# For more information check https://gitlab.com/gitlab-org/gitlab/-/issues/339348
stub_feature_flags(new_header_search: false)
+ # Disable the override flag in order to enable the feature by default.
+ # See https://docs.gitlab.com/ee/development/feature_flags/#selectively-disable-by-actor
+ stub_feature_flags(surface_environment_creation_failure_override: false)
+
allow(Gitlab::GitalyClient).to receive(:can_use_disk?).and_return(enable_rugged)
else
unstub_all_feature_flags
diff --git a/spec/tooling/quality/test_level_spec.rb b/spec/tooling/quality/test_level_spec.rb
index 89abe337347..0623a67a60e 100644
--- a/spec/tooling/quality/test_level_spec.rb
+++ b/spec/tooling/quality/test_level_spec.rb
@@ -63,7 +63,14 @@ RSpec.describe Quality::TestLevel do
context 'with a prefix' do
it 'returns a pattern' do
expect(described_class.new('ee/').pattern(:system))
- .to eq("ee/spec/{features}{,/**/}*_spec.rb")
+ .to eq("{ee/}spec/{features}{,/**/}*_spec.rb")
+ end
+ end
+
+ context 'with several prefixes' do
+ it 'returns a pattern' do
+ expect(described_class.new(['', 'ee/', 'jh/']).pattern(:system))
+ .to eq("{,ee/,jh/}spec/{features}{,/**/}*_spec.rb")
end
end
@@ -138,7 +145,14 @@ RSpec.describe Quality::TestLevel do
context 'with a prefix' do
it 'returns a regexp' do
expect(described_class.new('ee/').regexp(:system))
- .to eq(%r{ee/spec/(features)})
+ .to eq(%r{(ee/)spec/(features)})
+ end
+ end
+
+ context 'with several prefixes' do
+ it 'returns a regexp' do
+ expect(described_class.new(['', 'ee/', 'jh/']).regexp(:system))
+ .to eq(%r{(|ee/|jh/)spec/(features)})
end
end
diff --git a/tooling/quality/test_level.rb b/tooling/quality/test_level.rb
index ad9de067375..83cbe7a1f19 100644
--- a/tooling/quality/test_level.rb
+++ b/tooling/quality/test_level.rb
@@ -60,20 +60,20 @@ module Quality
system: ['features']
}.freeze
- attr_reader :prefix
+ attr_reader :prefixes
- def initialize(prefix = nil)
- @prefix = prefix
+ def initialize(prefixes = nil)
+ @prefixes = Array(prefixes)
@patterns = {}
@regexps = {}
end
def pattern(level)
- @patterns[level] ||= "#{prefix}spec/#{folders_pattern(level)}{,/**/}*#{suffix(level)}"
+ @patterns[level] ||= "#{prefixes_for_pattern}spec/#{folders_pattern(level)}{,/**/}*#{suffix(level)}"
end
def regexp(level)
- @regexps[level] ||= Regexp.new("#{prefix}spec/#{folders_regex(level)}").freeze
+ @regexps[level] ||= Regexp.new("#{prefixes_for_regex}spec/#{folders_regex(level)}").freeze
end
def level_for(file_path)
@@ -102,6 +102,20 @@ module Quality
private
+ def prefixes_for_pattern
+ return '' if prefixes.empty?
+
+ "{#{prefixes.join(',')}}"
+ end
+
+ def prefixes_for_regex
+ return '' if prefixes.empty?
+
+ regex_prefix = prefixes.map(&Regexp.method(:escape)).join('|')
+
+ "(#{regex_prefix})"
+ end
+
def suffix(level)
case level
when :frontend_fixture