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
path: root/spec
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2024-01-24 00:09:27 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2024-01-24 00:09:27 +0300
commit17bb9dd270c78fad45851c6cc6ec6e6fdb3d23bf (patch)
treeaa7235893811d97055b3fc750d139a039ae95b0a /spec
parentabd2c6b32aabff4654b6be9cb98b59dcd3193fc4 (diff)
Add latest changes from gitlab-org/gitlab@master
Diffstat (limited to 'spec')
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb43
-rw-r--r--spec/dot_gitlab_ci/rules_spec.rb14
-rw-r--r--spec/fixtures/gitlab/database/query_analyzers/large_query_with_in_list.txt1
-rw-r--r--spec/fixtures/gitlab/database/query_analyzers/small_query_with_in_list.txt1
-rw-r--r--spec/fixtures/gitlab/database/query_analyzers/small_query_without_in_list.txt1
-rw-r--r--spec/frontend/__helpers__/mock_observability_client.js1
-rw-r--r--spec/frontend/ci/runner/components/runner_cloud_form_spec.js16
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js123
-rw-r--r--spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js122
-rw-r--r--spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js122
-rw-r--r--spec/frontend/observability/client_spec.js192
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js22
-rw-r--r--spec/frontend/repository/mixins/highlight_mixin_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js191
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js330
-rw-r--r--spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb122
-rw-r--r--spec/lib/gitlab/ci/config/external/file/base_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb11
-rw-r--r--spec/lib/gitlab/ci/config/yaml/documents_spec.rb18
-rw-r--r--spec/lib/gitlab/ci/config/yaml/loader_spec.rb237
-rw-r--r--spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb37
-rw-r--r--spec/lib/gitlab/database/query_analyzer_spec.rb28
-rw-r--r--spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb2
-rw-r--r--spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb2
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb2
-rw-r--r--spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb2
-rw-r--r--spec/lib/gitlab/database/query_analyzers/log_large_in_lists_spec.rb148
-rw-r--r--spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb2
-rw-r--r--spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb2
-rw-r--r--spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb4
-rw-r--r--spec/models/packages/protection/rule_spec.rb11
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb4
-rw-r--r--spec/support/rspec_order_todo.yml1
33 files changed, 1088 insertions, 727 deletions
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
index a696eb933e9..abf12f0c3bf 100644
--- a/spec/controllers/projects/deployments_controller_spec.rb
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -84,6 +84,49 @@ RSpec.describe Projects::DeploymentsController do
end
end
+ describe 'GET #show' do
+ let(:deployment) { create(:deployment, :success, environment: environment) }
+
+ subject do
+ get :show, params: deployment_params(id: deployment.iid)
+ end
+
+ context 'without feature flag' do
+ before do
+ stub_feature_flags(deployment_details_page: false)
+ end
+
+ it 'renders a 404' do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+
+ context 'with feature flag' do
+ before do
+ stub_feature_flags(deployment_details_page: true)
+ end
+
+ context 'as maintainer' do
+ it 'renders show with 200 status code' do
+ is_expected.to have_gitlab_http_status(:ok)
+ is_expected.to render_template(:show)
+ end
+ end
+
+ context 'as anonymous user' do
+ let(:anonymous_user) { create(:user) }
+
+ before do
+ sign_in(anonymous_user)
+ end
+
+ it 'renders a 404' do
+ is_expected.to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+ end
+
describe 'GET #metrics' do
let(:deployment) { create(:deployment, :success, project: project, environment: environment) }
diff --git a/spec/dot_gitlab_ci/rules_spec.rb b/spec/dot_gitlab_ci/rules_spec.rb
index 88e98fe95ba..8e13be25f4c 100644
--- a/spec/dot_gitlab_ci/rules_spec.rb
+++ b/spec/dot_gitlab_ci/rules_spec.rb
@@ -3,10 +3,16 @@
require 'fast_spec_helper'
RSpec.describe '.gitlab/ci/rules.gitlab-ci.yml', feature_category: :tooling do
- config = YAML.load_file(
- File.expand_path('../../.gitlab/ci/rules.gitlab-ci.yml', __dir__),
- aliases: true
- ).freeze
+ begin
+ config = YAML.load_file(
+ File.expand_path('../../.gitlab/ci/rules.gitlab-ci.yml', __dir__),
+ aliases: true
+ ).freeze
+ rescue ArgumentError # Ruby 3.0 does not take `aliases: true`
+ config = YAML.load_file(
+ File.expand_path('../../.gitlab/ci/rules.gitlab-ci.yml', __dir__)
+ ).freeze
+ end
context 'with changes' do
config.each do |name, definition|
diff --git a/spec/fixtures/gitlab/database/query_analyzers/large_query_with_in_list.txt b/spec/fixtures/gitlab/database/query_analyzers/large_query_with_in_list.txt
new file mode 100644
index 00000000000..45d359ad457
--- /dev/null
+++ b/spec/fixtures/gitlab/database/query_analyzers/large_query_with_in_list.txt
@@ -0,0 +1 @@
+SELECT projects.id, projects.name FROM projects WHERE projects.namespace_id IN (SELECT DISTINCT namespaces.id FROM ((WITH base_ancestors_cte AS MATERIALIZED (SELECT namespaces.traversal_ids FROM ((WITH direct_groups AS MATERIALIZED (SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM ((SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM namespaces INNER JOIN members ON namespaces.id = members.source_id WHERE members.type = 'GroupMember' AND members.source_type = 'Namespace' AND namespaces.type = 'Group' AND members.user_id = 2167502 AND members.requested_at IS NULL AND (access_level >= 10)) UNION (SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM namespaces WHERE namespaces.type = 'Group' AND namespaces.id IN (SELECT projects.namespace_id FROM projects INNER JOIN project_authorizations ON projects.id = project_authorizations.project_id WHERE project_authorizations.user_id = 2167502))) namespaces WHERE namespaces.type = 'Group') SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM ((SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM direct_groups namespaces WHERE namespaces.type = 'Group') UNION (SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM namespaces INNER JOIN group_group_links ON group_group_links.shared_group_id = namespaces.id WHERE namespaces.type = 'Group' AND group_group_links.shared_with_group_id IN (SELECT namespaces.id FROM direct_groups namespaces WHERE namespaces.type = 'Group'))) namespaces WHERE namespaces.type = 'Group') UNION (SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM namespaces INNER JOIN members ON namespaces.id = members.source_id WHERE members.type = 'GroupMember' AND members.source_type = 'Namespace' AND namespaces.type = 'Group' AND members.user_id = 2167502 AND members.access_level = 5 AND (EXISTS (SELECT 1 FROM plans INNER JOIN gitlab_subscriptions ON gitlab_subscriptions.hosted_plan_id = plans.id WHERE plans.name IN ('silver', 'premium', 'premium_trial') AND (gitlab_subscriptions.namespace_id = namespaces.id))))) namespaces WHERE namespaces.type = 'Group') SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM namespaces INNER JOIN (SELECT DISTINCT unnest(base_ancestors_cte.traversal_ids) FROM base_ancestors_cte) AS ancestors(ancestor_id) ON namespaces.id = ancestors.ancestor_id WHERE namespaces.type = 'Group') UNION (WITH descendants_base_cte AS MATERIALIZED (SELECT namespaces.id, namespaces.traversal_ids FROM namespaces INNER JOIN members ON namespaces.id = members.source_id WHERE members.type = 'GroupMember' AND members.source_type = 'Namespace' AND namespaces.type = 'Group' AND members.user_id = 2167502 AND members.requested_at IS NULL AND (access_level >= 10)), superset AS (SELECT d1.traversal_ids FROM descendants_base_cte d1 WHERE NOT EXISTS ( SELECT 1 FROM descendants_base_cte d2 WHERE d2.id = ANY(d1.traversal_ids) AND d2.id <> d1.id ) ) SELECT DISTINCT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM superset, namespaces WHERE namespaces.type = 'Group' AND next_traversal_ids_sibling(superset.traversal_ids) > namespaces.traversal_ids AND superset.traversal_ids <= namespaces.traversal_ids) UNION (SELECT namespaces.id, namespaces.name, namespaces.path, namespaces.owner_id, namespaces.created_at, namespaces.updated_at, namespaces.type, namespaces.description, namespaces.avatar, namespaces.membership_lock, namespaces.share_with_group_lock, namespaces.visibility_level, namespaces.request_access_enabled, namespaces.ldap_sync_status, namespaces.ldap_sync_error, namespaces.ldap_sync_last_update_at, namespaces.ldap_sync_last_successful_update_at, namespaces.ldap_sync_last_sync_at, namespaces.lfs_enabled, namespaces.description_html, namespaces.parent_id, namespaces.shared_runners_minutes_limit, namespaces.repository_size_limit, namespaces.require_two_factor_authentication, namespaces.two_factor_grace_period, namespaces.cached_markdown_version, namespaces.project_creation_level, namespaces.runners_token, namespaces.file_template_project_id, namespaces.saml_discovery_token, namespaces.runners_token_encrypted, namespaces.custom_project_templates_group_id, namespaces.auto_devops_enabled, namespaces.extra_shared_runners_minutes_limit, namespaces.last_ci_minutes_notification_at, namespaces.last_ci_minutes_usage_notification_level, namespaces.subgroup_creation_level, namespaces.emails_disabled, namespaces.max_pages_size, namespaces.max_artifacts_size, namespaces.mentions_disabled, namespaces.default_branch_protection, namespaces.max_personal_access_token_lifetime, namespaces.push_rule_id, namespaces.shared_runners_enabled, namespaces.allow_descendants_override_disabled_shared_runners, namespaces.traversal_ids, namespaces.organization_id FROM namespaces WHERE namespaces.type = 'Group' AND namespaces.visibility_level IN (10, 20))) namespaces INNER JOIN projects ON projects.namespace_id = namespaces.id WHERE namespaces.type = 'Group' AND namespaces.id IN (SELECT namespaces.custom_project_templates_group_id FROM namespaces WHERE namespaces.type = 'Group' AND (traversal_ids[1] IN (SELECT gitlab_subscriptions.namespace_id FROM gitlab_subscriptions WHERE gitlab_subscriptions.hosted_plan_id IN (%IN_LIST%))) AND namespaces.custom_project_templates_group_id IS NOT NULL)) AND projects.marked_for_deletion_at IS NULL AND projects.pending_delete = FALSE AND projects.archived = FALSE;
diff --git a/spec/fixtures/gitlab/database/query_analyzers/small_query_with_in_list.txt b/spec/fixtures/gitlab/database/query_analyzers/small_query_with_in_list.txt
new file mode 100644
index 00000000000..df920489294
--- /dev/null
+++ b/spec/fixtures/gitlab/database/query_analyzers/small_query_with_in_list.txt
@@ -0,0 +1 @@
+SELECT namespaces.id FROM namespaces WHERE namespaces.id IN (%IN_LIST%)
diff --git a/spec/fixtures/gitlab/database/query_analyzers/small_query_without_in_list.txt b/spec/fixtures/gitlab/database/query_analyzers/small_query_without_in_list.txt
new file mode 100644
index 00000000000..982159eb40c
--- /dev/null
+++ b/spec/fixtures/gitlab/database/query_analyzers/small_query_without_in_list.txt
@@ -0,0 +1 @@
+SELECT 1 FROM namespaces;
diff --git a/spec/frontend/__helpers__/mock_observability_client.js b/spec/frontend/__helpers__/mock_observability_client.js
index a65b5233b73..571ee68f9bf 100644
--- a/spec/frontend/__helpers__/mock_observability_client.js
+++ b/spec/frontend/__helpers__/mock_observability_client.js
@@ -4,6 +4,7 @@ export function createMockClient() {
const mockClient = buildClient({
provisioningUrl: 'provisioning-url',
tracingUrl: 'tracing-url',
+ tracingAnalyticsUrl: 'tracing-analytics-url',
servicesUrl: 'services-url',
operationsUrl: 'operations-url',
metricsUrl: 'metrics-url',
diff --git a/spec/frontend/ci/runner/components/runner_cloud_form_spec.js b/spec/frontend/ci/runner/components/runner_cloud_form_spec.js
new file mode 100644
index 00000000000..ae856631f60
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_cloud_form_spec.js
@@ -0,0 +1,16 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerCloudConnectionForm from '~/ci/runner/components/runner_cloud_connection_form.vue';
+
+describe('Runner Cloud Form', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(RunnerCloudConnectionForm);
+ };
+
+ it('default', () => {
+ createComponent();
+
+ expect(wrapper.exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
index eddc1438fff..18aa722b94a 100644
--- a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
@@ -22,65 +22,108 @@ describe('RunnerPlatformsRadioGroup', () => {
.filter((w) => w.text() === text)
.at(0);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ const createComponent = ({
+ props = {},
+ mountFn = shallowMountExtended,
+ gcpRunner = false,
+ ...options
+ } = {}) => {
wrapper = mountFn(RunnerPlatformsRadioGroup, {
propsData: {
value: null,
...props,
},
+ provide: {
+ glFeatures: {
+ gcpRunner,
+ },
+ },
...options,
});
};
- beforeEach(() => {
- createComponent();
- });
+ describe('defaults', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- it('contains expected options with images', () => {
- const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
+ it('contains expected options with images', () => {
+ const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
- expect(labels).toEqual([
- ['Linux', expect.any(String)],
- ['macOS', null],
- ['Windows', null],
- ['Docker', expect.any(String)],
- ['Kubernetes', expect.any(String)],
- ]);
- });
+ expect(labels).toStrictEqual([
+ ['Linux', expect.any(String)],
+ ['macOS', null],
+ ['Windows', null],
+ ['Docker', expect.any(String)],
+ ['Kubernetes', expect.any(String)],
+ ]);
+ });
- it('allows users to use radio group', async () => {
- findFormRadioGroup().vm.$emit('input', MACOS_PLATFORM);
- await nextTick();
+ it('allows users to use radio group', async () => {
+ findFormRadioGroup().vm.$emit('input', MACOS_PLATFORM);
+ await nextTick();
- expect(wrapper.emitted('input')[0]).toEqual([MACOS_PLATFORM]);
- });
+ expect(wrapper.emitted('input')[0]).toEqual([MACOS_PLATFORM]);
+ });
+
+ it.each`
+ text | value
+ ${'Linux'} | ${LINUX_PLATFORM}
+ ${'macOS'} | ${MACOS_PLATFORM}
+ ${'Windows'} | ${WINDOWS_PLATFORM}
+ `('user can select "$text"', async ({ text, value }) => {
+ const radio = findFormRadioByText(text);
+ expect(radio.props('value')).toBe(value);
- it.each`
- text | value
- ${'Linux'} | ${LINUX_PLATFORM}
- ${'macOS'} | ${MACOS_PLATFORM}
- ${'Windows'} | ${WINDOWS_PLATFORM}
- `('user can select "$text"', async ({ text, value }) => {
- const radio = findFormRadioByText(text);
- expect(radio.props('value')).toBe(value);
+ radio.vm.$emit('input', value);
+ await nextTick();
- radio.vm.$emit('input', value);
- await nextTick();
+ expect(wrapper.emitted('input')[0]).toEqual([value]);
+ });
+
+ it.each`
+ text | href
+ ${'Docker'} | ${DOCKER_HELP_URL}
+ ${'Kubernetes'} | ${KUBERNETES_HELP_URL}
+ `('provides link to "$text" docs', ({ text, href }) => {
+ const radio = findFormRadioByText(text);
- expect(wrapper.emitted('input')[0]).toEqual([value]);
+ expect(radio.findComponent(GlLink).attributes()).toEqual({
+ href,
+ target: '_blank',
+ });
+ expect(radio.findComponent(GlIcon).props('name')).toBe('external-link');
+ });
});
- it.each`
- text | href
- ${'Docker'} | ${DOCKER_HELP_URL}
- ${'Kubernetes'} | ${KUBERNETES_HELP_URL}
- `('provides link to "$text" docs', ({ text, href }) => {
- const radio = findFormRadioByText(text);
+ describe('with gcpRunner flag enabled', () => {
+ it('contains expected options with images', () => {
+ createComponent({ props: {}, mountFn: shallowMountExtended, gcpRunner: true });
+
+ const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
+
+ expect(labels).toStrictEqual([
+ ['Linux', expect.any(String)],
+ ['macOS', null],
+ ['Windows', null],
+ ['Google Cloud', null],
+ ['Docker', expect.any(String)],
+ ['Kubernetes', expect.any(String)],
+ ]);
+ });
+
+ it('does not contain cloud option when admin prop is passed', () => {
+ createComponent({ props: { admin: true }, mountFn: shallowMountExtended, gcpRunner: true });
+
+ const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
- expect(radio.findComponent(GlLink).attributes()).toEqual({
- href,
- target: '_blank',
+ expect(labels).toStrictEqual([
+ ['Linux', expect.any(String)],
+ ['macOS', null],
+ ['Windows', null],
+ ['Docker', expect.any(String)],
+ ['Kubernetes', expect.any(String)],
+ ]);
});
- expect(radio.findComponent(GlIcon).props('name')).toBe('external-link');
});
});
diff --git a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
index 623a8f1c5a1..4e1e8c0adde 100644
--- a/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/group_new_runner_app/group_new_runner_app_spec.js
@@ -1,4 +1,5 @@
import { GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -13,7 +14,9 @@ import {
GROUP_TYPE,
DEFAULT_PLATFORM,
WINDOWS_PLATFORM,
+ GOOGLE_CLOUD_PLATFORM,
} from '~/ci/runner/constants';
+import RunnerCloudConnectionForm from '~/ci/runner/components/runner_cloud_connection_form.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import { runnerCreateResult } from '../mock_data';
@@ -36,8 +39,9 @@ describe('GroupRunnerRunnerApp', () => {
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
+ const findRunnerCloudForm = () => wrapper.findComponent(RunnerCloudConnectionForm);
- const createComponent = () => {
+ const createComponent = (gcpRunner = false) => {
wrapper = shallowMountExtended(GroupRunnerRunnerApp, {
propsData: {
groupId: mockGroupId,
@@ -45,74 +49,100 @@ describe('GroupRunnerRunnerApp', () => {
stubs: {
GlSprintf,
},
+ provide: {
+ glFeatures: {
+ gcpRunner,
+ },
+ },
});
};
- beforeEach(() => {
- createComponent();
- });
-
- it('shows a registration compatibility alert', () => {
- expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockGroupId);
- });
+ describe('defaults', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- describe('Platform', () => {
- it('shows the platforms radio group', () => {
- expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockGroupId);
});
- });
- describe('Runner form', () => {
- it('shows the runner create form for an instance runner', () => {
- expect(findRunnerCreateForm().props()).toEqual({
- runnerType: GROUP_TYPE,
- groupId: mockGroupId,
- projectId: null,
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
});
});
- describe('When a runner is saved', () => {
- beforeEach(() => {
- findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: GROUP_TYPE,
+ groupId: mockGroupId,
+ projectId: null,
+ });
});
- it('pushes an alert to be shown after redirection', () => {
- expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
- message: s__('Runners|Runner created.'),
- variant: VARIANT_SUCCESS,
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
});
- });
- it('redirects to the registration page', () => {
- const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
+ });
+ });
- expect(visitUrl).toHaveBeenCalledWith(url);
- });
- });
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
- describe('When another platform is selected and a runner is saved', () => {
- beforeEach(() => {
- findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
- findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ expect(visitUrl).toHaveBeenCalledWith(url);
+ });
});
- it('redirects to the registration page with the platform', () => {
- const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
- expect(visitUrl).toHaveBeenCalledWith(url);
+ expect(visitUrl).toHaveBeenCalledWith(url);
+ });
});
- });
- describe('When runner fails to save', () => {
- const ERROR_MSG = 'Cannot save!';
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
- beforeEach(() => {
- findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
- });
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
- it('shows an error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
});
});
});
+
+ describe('Runner cloud form', () => {
+ it.each`
+ flagState | visible
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'shows runner cloud form: $visible when flag is set to $flagState and platform is google',
+ async ({ flagState, visible }) => {
+ createComponent(flagState);
+
+ findRunnerPlatformsRadioGroup().vm.$emit('input', GOOGLE_CLOUD_PLATFORM);
+
+ await nextTick();
+
+ expect(findRunnerCloudForm().exists()).toBe(visible);
+ },
+ );
+ });
});
diff --git a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
index 3e12f3911a0..e2cbe731032 100644
--- a/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
+++ b/spec/frontend/ci/runner/project_new_runner_app/project_new_runner_app_spec.js
@@ -1,4 +1,5 @@
import { GlSprintf } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -13,7 +14,9 @@ import {
PROJECT_TYPE,
DEFAULT_PLATFORM,
WINDOWS_PLATFORM,
+ GOOGLE_CLOUD_PLATFORM,
} from '~/ci/runner/constants';
+import RunnerCloudConnectionForm from '~/ci/runner/components/runner_cloud_connection_form.vue';
import RunnerCreateForm from '~/ci/runner/components/runner_create_form.vue';
import { visitUrl } from '~/lib/utils/url_utility';
import { runnerCreateResult, mockRegistrationToken } from '../mock_data';
@@ -36,8 +39,9 @@ describe('ProjectRunnerRunnerApp', () => {
const findRegistrationCompatibilityAlert = () =>
wrapper.findComponent(RegistrationCompatibilityAlert);
const findRunnerCreateForm = () => wrapper.findComponent(RunnerCreateForm);
+ const findRunnerCloudForm = () => wrapper.findComponent(RunnerCloudConnectionForm);
- const createComponent = () => {
+ const createComponent = (gcpRunner = false) => {
wrapper = shallowMountExtended(ProjectRunnerRunnerApp, {
propsData: {
projectId: mockProjectId,
@@ -46,74 +50,100 @@ describe('ProjectRunnerRunnerApp', () => {
stubs: {
GlSprintf,
},
+ provide: {
+ glFeatures: {
+ gcpRunner,
+ },
+ },
});
};
- beforeEach(() => {
- createComponent();
- });
-
- it('shows a registration compatibility alert', () => {
- expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockProjectId);
- });
+ describe('defaults', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- describe('Platform', () => {
- it('shows the platforms radio group', () => {
- expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ it('shows a registration compatibility alert', () => {
+ expect(findRegistrationCompatibilityAlert().props('alertKey')).toBe(mockProjectId);
});
- });
- describe('Runner form', () => {
- it('shows the runner create form for an instance runner', () => {
- expect(findRunnerCreateForm().props()).toEqual({
- runnerType: PROJECT_TYPE,
- projectId: mockProjectId,
- groupId: null,
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
});
});
- describe('When a runner is saved', () => {
- beforeEach(() => {
- findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ describe('Runner form', () => {
+ it('shows the runner create form for an instance runner', () => {
+ expect(findRunnerCreateForm().props()).toEqual({
+ runnerType: PROJECT_TYPE,
+ projectId: mockProjectId,
+ groupId: null,
+ });
});
- it('pushes an alert to be shown after redirection', () => {
- expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
- message: s__('Runners|Runner created.'),
- variant: VARIANT_SUCCESS,
+ describe('When a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
});
- });
- it('redirects to the registration page', () => {
- const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
+ it('pushes an alert to be shown after redirection', () => {
+ expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
+ message: s__('Runners|Runner created.'),
+ variant: VARIANT_SUCCESS,
+ });
+ });
- expect(visitUrl).toHaveBeenCalledWith(url);
- });
- });
+ it('redirects to the registration page', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${DEFAULT_PLATFORM}`;
- describe('When another platform is selected and a runner is saved', () => {
- beforeEach(() => {
- findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
- findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ expect(visitUrl).toHaveBeenCalledWith(url);
+ });
});
- it('redirects to the registration page with the platform', () => {
- const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
+ describe('When another platform is selected and a runner is saved', () => {
+ beforeEach(() => {
+ findRunnerPlatformsRadioGroup().vm.$emit('input', WINDOWS_PLATFORM);
+ findRunnerCreateForm().vm.$emit('saved', mockCreatedRunner);
+ });
+
+ it('redirects to the registration page with the platform', () => {
+ const url = `${mockCreatedRunner.ephemeralRegisterUrl}?${PARAM_KEY_PLATFORM}=${WINDOWS_PLATFORM}`;
- expect(visitUrl).toHaveBeenCalledWith(url);
+ expect(visitUrl).toHaveBeenCalledWith(url);
+ });
});
- });
- describe('When runner fails to save', () => {
- const ERROR_MSG = 'Cannot save!';
+ describe('When runner fails to save', () => {
+ const ERROR_MSG = 'Cannot save!';
- beforeEach(() => {
- findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
- });
+ beforeEach(() => {
+ findRunnerCreateForm().vm.$emit('error', new Error(ERROR_MSG));
+ });
- it('shows an error message', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: ERROR_MSG });
+ });
});
});
});
+
+ describe('Runner cloud form', () => {
+ it.each`
+ flagState | visible
+ ${true} | ${true}
+ ${false} | ${false}
+ `(
+ 'shows runner cloud form: $visible when flag is set to $flagState and platform is google',
+ async ({ flagState, visible }) => {
+ createComponent(flagState);
+
+ findRunnerPlatformsRadioGroup().vm.$emit('input', GOOGLE_CLOUD_PLATFORM);
+
+ await nextTick();
+
+ expect(findRunnerCloudForm().exists()).toBe(visible);
+ },
+ );
+ });
});
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 1bd0112746b..e3196861659 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -14,6 +14,7 @@ describe('buildClient', () => {
let axiosMock;
const tracingUrl = 'https://example.com/tracing';
+ const tracingAnalyticsUrl = 'https://example.com/tracing/analytics';
const provisioningUrl = 'https://example.com/provisioning';
const servicesUrl = 'https://example.com/services';
const operationsUrl = 'https://example.com/services/$SERVICE_NAME$/operations';
@@ -23,6 +24,7 @@ describe('buildClient', () => {
const apiConfig = {
tracingUrl,
+ tracingAnalyticsUrl,
provisioningUrl,
servicesUrl,
operationsUrl,
@@ -389,6 +391,196 @@ describe('buildClient', () => {
});
});
+ describe('fetchTracesAnalytics', () => {
+ it('fetches analytics from the tracesAnalytics URL', async () => {
+ const mockResponse = {
+ results: [
+ {
+ Interval: 1705039800,
+ count: 5,
+ p90_duration_nano: 50613502867,
+ p95_duration_nano: 50613502867,
+ p75_duration_nano: 49756727928,
+ p50_duration_nano: 41610120929,
+ error_count: 324,
+ trace_rate: 2.576111111111111,
+ error_rate: 0.09,
+ },
+ ],
+ };
+
+ axiosMock.onGet(tracingAnalyticsUrl).reply(200, mockResponse);
+
+ const result = await client.fetchTracesAnalytics();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(tracingAnalyticsUrl, {
+ withCredentials: true,
+ params: expect.any(URLSearchParams),
+ });
+ expect(result).toEqual(mockResponse.results);
+ });
+
+ it('returns empty array if analytics are missing', async () => {
+ axiosMock.onGet(tracingAnalyticsUrl).reply(200, {});
+
+ expect(await client.fetchTracesAnalytics()).toEqual([]);
+ });
+
+ describe('query filter', () => {
+ beforeEach(() => {
+ axiosMock.onGet(tracingAnalyticsUrl).reply(200, {
+ results: [],
+ });
+ });
+
+ it('does not set any query param without filters', async () => {
+ await client.fetchTracesAnalytics();
+
+ expect(getQueryParam()).toBe(``);
+ });
+
+ it('converts filter to proper query params', async () => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ durationMs: [
+ { operator: '>', value: '100' },
+ { operator: '<', value: '1000' },
+ ],
+ operation: [
+ { operator: '=', value: 'op' },
+ { operator: '!=', value: 'not-op' },
+ ],
+ service: [
+ { operator: '=', value: 'service' },
+ { operator: '!=', value: 'not-service' },
+ ],
+ period: [{ operator: '=', value: '5m' }],
+ status: [
+ { operator: '=', value: 'ok' },
+ { operator: '!=', value: 'error' },
+ ],
+ traceId: [
+ { operator: '=', value: 'trace-id' },
+ { operator: '!=', value: 'not-trace-id' },
+ ],
+ attribute: [{ operator: '=', value: 'name1=value1' }],
+ },
+ });
+ expect(getQueryParam()).toContain(
+ 'gt[duration_nano]=100000000&lt[duration_nano]=1000000000' +
+ '&operation=op&not[operation]=not-op' +
+ '&service_name=service&not[service_name]=not-service' +
+ '&period=5m' +
+ '&trace_id=trace-id&not[trace_id]=not-trace-id' +
+ '&attr_name=name1&attr_value=value1' +
+ '&status=ok&not[status]=error',
+ );
+ });
+ describe('date range time filter', () => {
+ it('handles custom date range period filter', async () => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ period: [{ operator: '=', value: '2023-01-01 - 2023-02-01' }],
+ },
+ });
+ expect(getQueryParam()).not.toContain('period=');
+ expect(getQueryParam()).toContain(
+ 'start_time=2023-01-01T00:00:00.000Z&end_time=2023-02-01T00:00:00.000Z',
+ );
+ });
+
+ it.each([
+ 'invalid - 2023-02-01',
+ '2023-02-01 - invalid',
+ 'invalid - invalid',
+ '2023-01-01 / 2023-02-01',
+ '2023-01-01 2023-02-01',
+ '2023-01-01 - 2023-02-01 - 2023-02-01',
+ ])('ignore invalid values', async (val) => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ period: [{ operator: '=', value: val }],
+ },
+ });
+
+ expect(getQueryParam()).not.toContain('start_time=');
+ expect(getQueryParam()).not.toContain('end_time=');
+ expect(getQueryParam()).not.toContain('period=');
+ });
+ });
+
+ it('handles repeated params', async () => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ operation: [
+ { operator: '=', value: 'op' },
+ { operator: '=', value: 'op2' },
+ ],
+ },
+ });
+ expect(getQueryParam()).toContain('operation=op&operation=op2');
+ });
+
+ it('ignores unsupported filters', async () => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ unsupportedFilter: [{ operator: '=', value: 'foo' }],
+ },
+ });
+
+ expect(getQueryParam()).toBe(``);
+ });
+
+ it('ignores empty filters', async () => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ durationMs: null,
+ },
+ });
+
+ expect(getQueryParam()).toBe(``);
+ });
+
+ it('ignores non-array filters', async () => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ traceId: { operator: '=', value: 'foo' },
+ },
+ });
+
+ expect(getQueryParam()).toBe(``);
+ });
+
+ it('ignores unsupported operators', async () => {
+ await client.fetchTracesAnalytics({
+ filters: {
+ durationMs: [
+ { operator: '*', value: 'foo' },
+ { operator: '=', value: 'foo' },
+ { operator: '!=', value: 'foo' },
+ ],
+ operation: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ service: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ period: [{ operator: '!=', value: 'foo' }],
+ traceId: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ },
+ });
+
+ expect(getQueryParam()).toBe(``);
+ });
+ });
+ });
+
describe('fetchServices', () => {
it('fetches services from the services URL', async () => {
const mockResponse = {
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 8414dfcf151..31337364dca 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -18,7 +18,6 @@ import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
-import SourceViewerNew from '~/vue_shared/components/source_viewer/source_viewer_new.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import CodeIntelligence from '~/code_navigation/components/app.vue';
@@ -60,6 +59,7 @@ const mockRouterPush = jest.fn();
const mockRouter = {
push: mockRouterPush,
};
+const highlightWorker = { postMessage: jest.fn() };
const legacyViewerUrl = '/some_file.js?format=json&viewer=simple';
@@ -74,7 +74,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
downloadCode = userPermissionsMock.downloadCode,
createMergeRequestIn = userPermissionsMock.createMergeRequestIn,
isBinary,
- inject = {},
+ inject = { highlightWorker },
} = mockData;
const blobInfo = {
@@ -136,9 +136,6 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
targetBranch: 'test',
originalBranch: 'default-ref',
...inject,
- glFeatures: {
- highlightJsWorker: false,
- },
},
}),
);
@@ -158,7 +155,6 @@ describe('Blob content viewer component', () => {
const findForkSuggestion = () => wrapper.findComponent(ForkSuggestion);
const findCodeIntelligence = () => wrapper.findComponent(CodeIntelligence);
const findSourceViewer = () => wrapper.findComponent(SourceViewer);
- const findSourceViewerNew = () => wrapper.findComponent(SourceViewerNew);
beforeEach(() => {
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
@@ -203,28 +199,28 @@ describe('Blob content viewer component', () => {
});
it('adds blame param to the URL and passes `showBlame` to the SourceViewer', async () => {
- loadViewer.mockReturnValueOnce(SourceViewerNew);
+ loadViewer.mockReturnValueOnce(SourceViewer);
await createComponent({ blob: simpleViewerMock });
await triggerBlame();
expect(mockRouterPush).toHaveBeenCalledWith({ query: { blame: '1' } });
- expect(findSourceViewerNew().props('showBlame')).toBe(true);
+ expect(findSourceViewer().props('showBlame')).toBe(true);
await triggerBlame();
expect(mockRouterPush).toHaveBeenCalledWith({ query: { blame: '0' } });
- expect(findSourceViewerNew().props('showBlame')).toBe(false);
+ expect(findSourceViewer().props('showBlame')).toBe(false);
});
describe('when viewing rich content', () => {
it('always shows the blame when clicking on the blame button', async () => {
- loadViewer.mockReturnValueOnce(SourceViewerNew);
+ loadViewer.mockReturnValueOnce(SourceViewer);
const query = { plain: '0', blame: '1' };
await createComponent({ blob: simpleViewerMock }, shallowMount, { query });
await triggerBlame();
- expect(findSourceViewerNew().props('showBlame')).toBe(true);
+ expect(findSourceViewer().props('showBlame')).toBe(true);
});
});
});
@@ -435,7 +431,7 @@ describe('Blob content viewer component', () => {
await waitForPromises();
- expect(loadViewer).toHaveBeenCalledWith(viewer, false, false, 'javascript');
+ expect(loadViewer).toHaveBeenCalledWith(viewer, false);
expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
});
});
@@ -514,7 +510,7 @@ describe('Blob content viewer component', () => {
});
it('is called with originalBranch value if the prop has a value', async () => {
- await createComponent({ inject: { originalBranch: 'some-branch' } });
+ await createComponent({ inject: { originalBranch: 'some-branch', highlightWorker } });
expect(blobInfoMockResolver).toHaveBeenCalledWith(
expect.objectContaining({
diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js
index c635c09d1aa..ccde41f62e5 100644
--- a/spec/frontend/repository/mixins/highlight_mixin_spec.js
+++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js
@@ -41,7 +41,6 @@ describe('HighlightMixin', () => {
mixins: [highlightMixin],
inject: {
highlightWorker: { default: workerMock },
- glFeatures: { default: { highlightJsWorker: true } },
},
template: '<div>{{chunks[0]?.highlightedContent}}</div>',
created() {
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
deleted file mode 100644
index 745886161ce..00000000000
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
+++ /dev/null
@@ -1,191 +0,0 @@
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { setHTMLFixture } from 'helpers/fixtures';
-import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_new.vue';
-import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
-import {
- EVENT_ACTION,
- EVENT_LABEL_VIEWER,
- CODEOWNERS_FILE_NAME,
-} from '~/vue_shared/components/source_viewer/constants';
-import Tracking from '~/tracking';
-import LineHighlighter from '~/blob/line_highlighter';
-import addBlobLinksTracking from '~/blob/blob_links_tracking';
-import waitForPromises from 'helpers/wait_for_promises';
-import blameDataQuery from '~/vue_shared/components/source_viewer/queries/blame_data.query.graphql';
-import Blame from '~/vue_shared/components/source_viewer/components/blame_info.vue';
-import * as utils from '~/vue_shared/components/source_viewer/utils';
-import CodeownersValidation from 'ee_component/blob/components/codeowners_validation.vue';
-
-import {
- BLOB_DATA_MOCK,
- CHUNK_1,
- CHUNK_2,
- LANGUAGE_MOCK,
- BLAME_DATA_QUERY_RESPONSE_MOCK,
- SOURCE_CODE_CONTENT_MOCK,
-} from './mock_data';
-
-Vue.use(VueApollo);
-
-const lineHighlighter = new LineHighlighter();
-jest.mock('~/blob/line_highlighter', () =>
- jest.fn().mockReturnValue({
- highlightHash: jest.fn(),
- }),
-);
-jest.mock('~/blob/blob_links_tracking');
-
-describe('Source Viewer component', () => {
- let wrapper;
- let fakeApollo;
- const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
- const projectPath = 'test';
- const currentRef = 'main';
- const hash = '#L142';
-
- const blameDataQueryHandlerSuccess = jest.fn().mockResolvedValue(BLAME_DATA_QUERY_RESPONSE_MOCK);
- const blameInfo =
- BLAME_DATA_QUERY_RESPONSE_MOCK.data.project.repository.blobs.nodes[0].blame.groups;
-
- const createComponent = ({ showBlame = true, blob = {} } = {}) => {
- fakeApollo = createMockApollo([[blameDataQuery, blameDataQueryHandlerSuccess]]);
-
- wrapper = shallowMountExtended(SourceViewer, {
- apolloProvider: fakeApollo,
- mocks: { $route: { hash } },
- propsData: {
- blob: { ...blob, ...BLOB_DATA_MOCK },
- chunks: CHUNKS_MOCK,
- projectPath,
- currentRef,
- showBlame,
- },
- });
- };
-
- const findChunks = () => wrapper.findAllComponents(Chunk);
- const findBlameComponents = () => wrapper.findAllComponents(Blame);
- const triggerChunkAppear = async (chunkIndex = 0) => {
- findChunks().at(chunkIndex).vm.$emit('appear');
- await waitForPromises();
- };
-
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
- return createComponent();
- });
-
- it('instantiates the lineHighlighter class', () => {
- expect(LineHighlighter).toHaveBeenCalled();
- });
-
- describe('event tracking', () => {
- it('fires a tracking event when the component is created', () => {
- const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- });
-
- it('adds blob links tracking', () => {
- expect(addBlobLinksTracking).toHaveBeenCalled();
- });
- });
-
- describe('rendering', () => {
- it('does not render a Blame component if the respective chunk for the blame has not appeared', async () => {
- await waitForPromises();
- expect(findBlameComponents()).toHaveLength(0);
- });
-
- describe('DOM updates', () => {
- it('adds the necessary classes to the DOM', async () => {
- setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
- jest.spyOn(utils, 'toggleBlameClasses');
- createComponent();
- await triggerChunkAppear();
-
- expect(utils.toggleBlameClasses).toHaveBeenCalledWith(blameInfo, true);
- });
- });
-
- describe('Blame information', () => {
- it('renders a Blame component when a chunk appears', async () => {
- await triggerChunkAppear();
-
- expect(findBlameComponents().at(0).exists()).toBe(true);
- expect(findBlameComponents().at(0).props()).toMatchObject({ blameInfo });
- });
-
- it('calls the blame data query', async () => {
- await triggerChunkAppear();
-
- expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
- expect.objectContaining({
- filePath: BLOB_DATA_MOCK.path,
- fullPath: projectPath,
- ref: currentRef,
- }),
- );
- });
-
- it('calls the query only once per chunk', async () => {
- // We trigger the `appear` event multiple times here in order to simulate the user scrolling past the chunk more than once.
- // In this scenario we only want to query the backend once.
- await triggerChunkAppear();
- await triggerChunkAppear();
-
- expect(blameDataQueryHandlerSuccess).toHaveBeenCalledTimes(1);
- });
-
- it('requests blame information for overlapping chunk', async () => {
- await triggerChunkAppear(1);
-
- expect(blameDataQueryHandlerSuccess).toHaveBeenCalledTimes(2);
- expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
- expect.objectContaining({ fromLine: 71, toLine: 110 }),
- );
- expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
- expect.objectContaining({ fromLine: 1, toLine: 70 }),
- );
-
- expect(findChunks().at(0).props('isHighlighted')).toBe(true);
- });
-
- it('does not render a Blame component when `showBlame: false`', async () => {
- createComponent({ showBlame: false });
- await triggerChunkAppear();
-
- expect(findBlameComponents()).toHaveLength(0);
- });
- });
-
- it('renders a Chunk component for each chunk', () => {
- expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
- expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
- });
- });
-
- describe('hash highlighting', () => {
- it('calls highlightHash with expected parameter', () => {
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
- });
- });
-
- describe('Codeowners validation', () => {
- const findCodeownersValidation = () => wrapper.findComponent(CodeownersValidation);
-
- it('does not render codeowners validation when file is not CODEOWNERS', async () => {
- await createComponent();
- await nextTick();
- expect(findCodeownersValidation().exists()).toBe(false);
- });
-
- it('renders codeowners validation when file is CODEOWNERS', async () => {
- await createComponent({ blob: { name: CODEOWNERS_FILE_NAME } });
- await nextTick();
- expect(findCodeownersValidation().exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 2043f36443d..1fa15b28cf1 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -1,279 +1,175 @@
-import hljs from 'highlight.js/lib/core';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture } from 'helpers/fixtures';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
-import CodeownersValidation from 'ee_component/blob/components/codeowners_validation.vue';
-import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
-import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
import {
EVENT_ACTION,
EVENT_LABEL_VIEWER,
- EVENT_LABEL_FALLBACK,
- ROUGE_TO_HLJS_LANGUAGE_MAP,
- LINES_PER_CHUNK,
- LEGACY_FALLBACKS,
CODEOWNERS_FILE_NAME,
- CODEOWNERS_LANGUAGE,
- SVELTE_LANGUAGE,
} from '~/vue_shared/components/source_viewer/constants';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import waitForPromises from 'helpers/wait_for_promises';
-import LineHighlighter from '~/blob/line_highlighter';
-import eventHub from '~/notes/event_hub';
import Tracking from '~/tracking';
+import LineHighlighter from '~/blob/line_highlighter';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import waitForPromises from 'helpers/wait_for_promises';
+import blameDataQuery from '~/vue_shared/components/source_viewer/queries/blame_data.query.graphql';
+import Blame from '~/vue_shared/components/source_viewer/components/blame_info.vue';
+import * as utils from '~/vue_shared/components/source_viewer/utils';
+import CodeownersValidation from 'ee_component/blob/components/codeowners_validation.vue';
-const lineHighlighter = new LineHighlighter();
-jest.mock('~/blob/line_highlighter', () => jest.fn().mockReturnValue({ highlightHash: jest.fn() }));
-jest.mock('highlight.js/lib/core');
-jest.mock('~/vue_shared/components/source_viewer/plugins/index');
-const mockAxios = new MockAdapter(axios);
+import {
+ BLOB_DATA_MOCK,
+ CHUNK_1,
+ CHUNK_2,
+ LANGUAGE_MOCK,
+ BLAME_DATA_QUERY_RESPONSE_MOCK,
+ SOURCE_CODE_CONTENT_MOCK,
+} from './mock_data';
-const generateContent = (content, totalLines = 1, delimiter = '\n') => {
- let generatedContent = '';
- for (let i = 0; i < totalLines; i += 1) {
- generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
- }
- return generatedContent;
-};
+Vue.use(VueApollo);
-const execImmediately = (callback) => callback();
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
+jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
- const language = 'docker';
- const selectedRangeHash = '#L1-2';
- const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
- const chunk1 = generateContent('// Some source code 1', 70);
- const chunk2 = generateContent('// Some source code 2', 70);
- const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
- const chunk3Result = generateContent('// Some source code 3', 70, '\n');
- const content = chunk1 + chunk2 + chunk3;
- const path = 'some/path.js';
- const blamePath = 'some/blame/path.js';
- const fileType = 'javascript';
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
- const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+ let fakeApollo;
+ const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
+ const projectPath = 'test';
const currentRef = 'main';
- const projectPath = 'test/project';
+ const hash = '#L142';
+
+ const blameDataQueryHandlerSuccess = jest.fn().mockResolvedValue(BLAME_DATA_QUERY_RESPONSE_MOCK);
+ const blameInfo =
+ BLAME_DATA_QUERY_RESPONSE_MOCK.data.project.repository.blobs.nodes[0].blame.groups;
+
+ const createComponent = ({ showBlame = true, blob = {} } = {}) => {
+ fakeApollo = createMockApollo([[blameDataQuery, blameDataQueryHandlerSuccess]]);
- const createComponent = async (blob = {}) => {
wrapper = shallowMountExtended(SourceViewer, {
- propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob }, currentRef, projectPath },
- mocks: { $route: { hash: selectedRangeHash } },
+ apolloProvider: fakeApollo,
+ mocks: { $route: { hash } },
+ propsData: {
+ blob: { ...blob, ...BLOB_DATA_MOCK },
+ chunks: CHUNKS_MOCK,
+ projectPath,
+ currentRef,
+ showBlame,
+ },
});
- await waitForPromises();
};
const findChunks = () => wrapper.findAllComponents(Chunk);
+ const findBlameComponents = () => wrapper.findAllComponents(Blame);
+ const triggerChunkAppear = async (chunkIndex = 0) => {
+ findChunks().at(chunkIndex).vm.$emit('appear');
+ await waitForPromises();
+ };
beforeEach(() => {
- hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
- hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
- jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(eventHub, '$emit');
jest.spyOn(Tracking, 'event');
-
return createComponent();
});
- describe('Displaying LFS blob', () => {
- const rawPath = '/org/project/-/raw/file.xml';
- const externalStorageUrl = 'http://127.0.0.1:9000/lfs-objects/91/12/1341234';
- const rawTextBlob = 'This is the external content';
- const blob = {
- storedExternally: true,
- externalStorage: 'lfs',
- simpleViewer: { fileType: 'text' },
- rawPath,
- };
-
- afterEach(() => {
- mockAxios.reset();
- });
-
- it('Uses externalStorageUrl to fetch content if present', async () => {
- mockAxios.onGet(externalStorageUrl).replyOnce(HTTP_STATUS_OK, rawTextBlob);
-
- await createComponent({ ...blob, externalStorageUrl });
-
- expect(mockAxios.history.get).toHaveLength(1);
- expect(mockAxios.history.get[0].url).toBe(externalStorageUrl);
- expect(wrapper.vm.$data.content).toBe(rawTextBlob);
- });
-
- it('Falls back to rawPath to fetch content', async () => {
- mockAxios.onGet(rawPath).replyOnce(HTTP_STATUS_OK, rawTextBlob);
-
- await createComponent(blob);
-
- expect(mockAxios.history.get).toHaveLength(1);
- expect(mockAxios.history.get[0].url).toBe(rawPath);
- expect(wrapper.vm.$data.content).toBe(rawTextBlob);
- });
+ it('instantiates the lineHighlighter class', () => {
+ expect(LineHighlighter).toHaveBeenCalled();
});
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
- const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
});
- it('does not emit an error event when the language is supported', () => {
- expect(wrapper.emitted('error')).toBeUndefined();
+ it('adds blob links tracking', () => {
+ expect(addBlobLinksTracking).toHaveBeenCalled();
});
-
- it('fires a tracking event and emits an error when the language is not supported', () => {
- const unsupportedLanguage = 'apex';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
- createComponent({ language: unsupportedLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('legacy fallbacks', () => {
- it.each(LEGACY_FALLBACKS)(
- 'tracks a fallback event and emits an error when viewing %s files',
- (fallbackLanguage) => {
- const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
- createComponent({ language: fallbackLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- },
- );
});
- describe('highlight.js', () => {
- beforeEach(() => createComponent({ language: mappedLanguage }));
-
- it('registers our plugins for Highlight.js', () => {
- expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
- });
-
- it('registers the language definition', async () => {
- const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- mappedLanguage,
- languageDefinition.default,
- );
+ describe('rendering', () => {
+ it('does not render a Blame component if the respective chunk for the blame has not appeared', async () => {
+ await waitForPromises();
+ expect(findBlameComponents()).toHaveLength(0);
});
- describe('sub-languages', () => {
- const languageDefinition = {
- subLanguage: 'xml',
- contains: [{ subLanguage: 'javascript' }, { subLanguage: 'typescript' }],
- };
-
- beforeEach(async () => {
- jest.spyOn(hljs, 'getLanguage').mockReturnValue(languageDefinition);
+ describe('DOM updates', () => {
+ it('adds the necessary classes to the DOM', async () => {
+ setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
+ jest.spyOn(utils, 'toggleBlameClasses');
createComponent();
- await waitForPromises();
- });
+ await triggerChunkAppear();
- it('registers the primary sub-language', () => {
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- languageDefinition.subLanguage,
- expect.any(Function),
- );
+ expect(utils.toggleBlameClasses).toHaveBeenCalledWith(blameInfo, true);
});
-
- it.each(languageDefinition.contains)(
- 'registers the rest of the sub-languages',
- ({ subLanguage }) => {
- expect(hljs.registerLanguage).toHaveBeenCalledWith(subLanguage, expect.any(Function));
- },
- );
});
- it('registers json language definition if fileType is package_json', async () => {
- await createComponent({ language: 'json', fileType: 'package_json' });
- const languageDefinition = await import(`highlight.js/lib/languages/json`);
+ describe('Blame information', () => {
+ it('renders a Blame component when a chunk appears', async () => {
+ await triggerChunkAppear();
- expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
- });
-
- it('correctly maps languages starting with uppercase', async () => {
- await createComponent({ language: 'Ruby' });
- const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
- });
+ expect(findBlameComponents().at(0).exists()).toBe(true);
+ expect(findBlameComponents().at(0).props()).toMatchObject({ blameInfo });
+ });
- it('registers codeowners language definition if file name is CODEOWNERS', async () => {
- await createComponent({ name: CODEOWNERS_FILE_NAME });
- const languageDefinition = await import(
- '~/vue_shared/components/source_viewer/languages/codeowners'
- );
+ it('calls the blame data query', async () => {
+ await triggerChunkAppear();
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- CODEOWNERS_LANGUAGE,
- languageDefinition.default,
- );
- });
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
+ expect.objectContaining({
+ filePath: BLOB_DATA_MOCK.path,
+ fullPath: projectPath,
+ ref: currentRef,
+ }),
+ );
+ });
- it('registers svelte language definition if file name ends with .svelte', async () => {
- await createComponent({ name: `component.${SVELTE_LANGUAGE}` });
- const languageDefinition = await import(
- '~/vue_shared/components/source_viewer/languages/svelte'
- );
+ it('calls the query only once per chunk', async () => {
+ // We trigger the `appear` event multiple times here in order to simulate the user scrolling past the chunk more than once.
+ // In this scenario we only want to query the backend once.
+ await triggerChunkAppear();
+ await triggerChunkAppear();
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- SVELTE_LANGUAGE,
- languageDefinition.default,
- );
- });
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledTimes(1);
+ });
- it('highlights the first chunk', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
- expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
- });
+ it('requests blame information for overlapping chunk', async () => {
+ await triggerChunkAppear(1);
- describe('auto-detects if a language cannot be loaded', () => {
- beforeEach(() => createComponent({ language: 'some_unknown_language' }));
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledTimes(2);
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
+ expect.objectContaining({ fromLine: 71, toLine: 110 }),
+ );
+ expect(blameDataQueryHandlerSuccess).toHaveBeenCalledWith(
+ expect.objectContaining({ fromLine: 1, toLine: 70 }),
+ );
- it('highlights the content with auto-detection', () => {
- expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
+ expect(findChunks().at(0).props('isHighlighted')).toBe(true);
});
- });
- });
- describe('rendering', () => {
- it.each`
- chunkIndex | chunkContent | totalChunks
- ${0} | ${chunk1} | ${0}
- ${1} | ${chunk2} | ${3}
- ${2} | ${chunk3Result} | ${3}
- `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
- const chunk = findChunks().at(chunkIndex);
+ it('does not render a Blame component when `showBlame: false`', async () => {
+ createComponent({ showBlame: false });
+ await triggerChunkAppear();
- expect(chunk.props('content')).toContain(chunkContent.trim());
-
- expect(chunk.props()).toMatchObject({
- totalLines: LINES_PER_CHUNK,
- startingFrom: LINES_PER_CHUNK * chunkIndex,
- totalChunks,
+ expect(findBlameComponents()).toHaveLength(0);
});
});
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
+ it('renders a Chunk component for each chunk', () => {
+ expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
+ expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
});
});
- describe('LineHighlighter', () => {
- it('instantiates the lineHighlighter class', () => {
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
- });
-
- it('highlights the range when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- const scrollEnabled = false;
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(selectedRangeHash, scrollEnabled);
+ describe('hash highlighting', () => {
+ it('calls highlightHash with expected parameter', () => {
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
});
});
@@ -282,11 +178,13 @@ describe('Source Viewer component', () => {
it('does not render codeowners validation when file is not CODEOWNERS', async () => {
await createComponent();
+ await nextTick();
expect(findCodeownersValidation().exists()).toBe(false);
});
it('renders codeowners validation when file is CODEOWNERS', async () => {
- await createComponent({ name: CODEOWNERS_FILE_NAME });
+ await createComponent({ blob: { name: CODEOWNERS_FILE_NAME } });
+ await nextTick();
expect(findCodeownersValidation().exists()).toBe(true);
});
});
diff --git a/spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb b/spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb
deleted file mode 100644
index 73661a3da1f..00000000000
--- a/spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb
+++ /dev/null
@@ -1,122 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-# rubocop:disable Layout/HashAlignment
-RSpec.describe Gitlab::BackgroundMigration::BackfillProjectImportLevel do
- let(:migration) do
- described_class.new(
- start_id: table(:namespaces).minimum(:id),
- end_id: table(:namespaces).maximum(:id),
- batch_table: :namespaces,
- batch_column: :id,
- sub_batch_size: 2,
- pause_ms: 0,
- connection: ApplicationRecord.connection
- )
- end
- # rubocop:enable Layout/HashAlignment
-
- let(:namespaces_table) { table(:namespaces) }
- let(:namespace_settings_table) { table(:namespace_settings) }
-
- let!(:user_namespace) do
- namespaces_table.create!(
- name: 'user_namespace',
- path: 'user_namespace',
- type: 'User',
- project_creation_level: 100
- )
- end
-
- let!(:group_namespace_nil) do
- namespaces_table.create!(
- name: 'group_namespace_nil',
- path: 'group_namespace_nil',
- type: 'Group',
- project_creation_level: nil
- )
- end
-
- let!(:group_namespace_0) do
- namespaces_table.create!(
- name: 'group_namespace_0',
- path: 'group_namespace_0',
- type: 'Group',
- project_creation_level: 0
- )
- end
-
- let!(:group_namespace_1) do
- namespaces_table.create!(
- name: 'group_namespace_1',
- path: 'group_namespace_1',
- type: 'Group',
- project_creation_level: 1
- )
- end
-
- let!(:group_namespace_2) do
- namespaces_table.create!(
- name: 'group_namespace_2',
- path: 'group_namespace_2',
- type: 'Group',
- project_creation_level: 2
- )
- end
-
- let!(:group_namespace_9999) do
- namespaces_table.create!(
- name: 'group_namespace_9999',
- path: 'group_namespace_9999',
- type: 'Group',
- project_creation_level: 9999
- )
- end
-
- subject(:perform_migration) { migration.perform }
-
- before do
- namespace_settings_table.create!(namespace_id: user_namespace.id)
- namespace_settings_table.create!(namespace_id: group_namespace_nil.id)
- namespace_settings_table.create!(namespace_id: group_namespace_0.id)
- namespace_settings_table.create!(namespace_id: group_namespace_1.id)
- namespace_settings_table.create!(namespace_id: group_namespace_2.id)
- namespace_settings_table.create!(namespace_id: group_namespace_9999.id)
- end
-
- describe 'Groups' do
- using RSpec::Parameterized::TableSyntax
-
- where(:namespace_id, :prev_level, :new_level) do
- lazy { group_namespace_0.id } | ::Gitlab::Access::OWNER | ::Gitlab::Access::NO_ACCESS
- lazy { group_namespace_1.id } | ::Gitlab::Access::OWNER | ::Gitlab::Access::MAINTAINER
- lazy { group_namespace_2.id } | ::Gitlab::Access::OWNER | ::Gitlab::Access::DEVELOPER
- end
-
- with_them do
- it 'backfills the correct project_import_level of Group namespaces' do
- expect { perform_migration }
- .to change { namespace_settings_table.find_by(namespace_id: namespace_id).project_import_level }
- .from(prev_level).to(new_level)
- end
- end
-
- it 'does not update `User` namespaces or values outside range' do
- expect { perform_migration }
- .not_to change { namespace_settings_table.find_by(namespace_id: user_namespace.id).project_import_level }
-
- expect { perform_migration }
- .not_to change { namespace_settings_table.find_by(namespace_id: group_namespace_9999.id).project_import_level }
- end
-
- it 'maintains default import_level if creation_level is nil' do
- project_import_level = namespace_settings_table.find_by(namespace_id: group_namespace_nil.id).project_import_level
-
- expect { perform_migration }
- .not_to change { project_import_level }
-
- expect(project_import_level).to eq(::Gitlab::Access::OWNER)
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/external/file/base_spec.rb b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
index bcfab620bd9..6100974bbe8 100644
--- a/spec/lib/gitlab/ci/config/external/file/base_spec.rb
+++ b/spec/lib/gitlab/ci/config/external/file/base_spec.rb
@@ -145,7 +145,7 @@ RSpec.describe Gitlab::Ci::Config::External::File::Base, feature_category: :pipe
it 'surfaces interpolation errors' do
expect(valid?).to be_falsy
expect(file.errors)
- .to include('`some-location.yml`: interpolation interrupted by errors, unknown interpolation key: `abcd`')
+ .to include('`some-location.yml`: unknown interpolation key: `abcd`')
end
end
diff --git a/spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb b/spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb
index 8655d3fb0b7..2e59b6982dd 100644
--- a/spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb
+++ b/spec/lib/gitlab/ci/config/interpolation/text_interpolator_spec.rb
@@ -34,17 +34,6 @@ RSpec.describe Gitlab::Ci::Config::Interpolation::TextInterpolator, feature_cate
end
end
- context 'when the header has an error while being parsed' do
- let(:header) { ::Gitlab::Config::Loader::Yaml.new('_!@malformedyaml:&') }
-
- it 'surfaces the error' do
- interpolator.interpolate!
-
- expect(interpolator).not_to be_valid
- expect(interpolator.error_message).to eq('Invalid configuration format')
- end
- end
-
context 'when spec header is missing but inputs are specified' do
let(:documents) { ::Gitlab::Ci::Config::Yaml::Documents.new([content]) }
diff --git a/spec/lib/gitlab/ci/config/yaml/documents_spec.rb b/spec/lib/gitlab/ci/config/yaml/documents_spec.rb
index babdea6623b..424fa4858a4 100644
--- a/spec/lib/gitlab/ci/config/yaml/documents_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml/documents_spec.rb
@@ -5,24 +5,6 @@ require 'spec_helper'
RSpec.describe Gitlab::Ci::Config::Yaml::Documents, feature_category: :pipeline_composition do
let(:documents) { described_class.new(yaml_documents) }
- describe '#valid?' do
- context 'when there are no errors' do
- let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('job:')] }
-
- it 'returns true' do
- expect(documents).to be_valid
- end
- end
-
- context 'when there are errors' do
- let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('_!@malformedyaml:&')] }
-
- it 'returns false' do
- expect(documents).not_to be_valid
- end
- end
- end
-
describe '#header' do
context 'when there are at least 2 documents and the first document has a `spec` keyword' do
let(:yaml_documents) { [::Gitlab::Config::Loader::Yaml.new('spec:'), ::Gitlab::Config::Loader::Yaml.new('job:')] }
diff --git a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
index 684da1df43b..045cdf37037 100644
--- a/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
+++ b/spec/lib/gitlab/ci/config/yaml/loader_spec.rb
@@ -7,6 +7,7 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c
let_it_be(:project) { create(:project) }
let(:inputs) { { test_input: 'hello test' } }
+ let(:variables) { [] }
let(:yaml) do
<<~YAML
@@ -21,7 +22,7 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c
YAML
end
- subject(:result) { described_class.new(yaml, inputs: inputs).load }
+ subject(:result) { described_class.new(yaml, inputs: inputs, variables: variables).load }
it 'loads and interpolates CI config YAML' do
expected_config = { test_job: { script: ['echo "hello test"'] } }
@@ -57,6 +58,240 @@ RSpec.describe ::Gitlab::Ci::Config::Yaml::Loader, feature_category: :pipeline_c
expect(result.error).to eq('`test_input` input: required value has not been provided')
end
end
+
+ context 'when interpolating into a YAML key' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ ---
+ "$[[ inputs.test_input ]]_job":
+ script:
+ - echo "test"
+ YAML
+ end
+
+ it 'loads and interpolates CI config YAML' do
+ expected_config = { 'hello test_job': { script: ['echo "test"'] } }
+
+ expect(result).to be_valid
+ expect(result).to be_interpolated
+ expect(result.content).to eq(expected_config)
+ end
+ end
+
+ context 'when interpolating values of different types' do
+ let(:inputs) do
+ {
+ test_boolean: true,
+ test_number: 8,
+ test_string: 'test'
+ }
+ end
+
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_string:
+ type: string
+ test_boolean:
+ type: boolean
+ test_number:
+ type: number
+ ---
+ "$[[ inputs.test_string ]]_job":
+ allow_failure: $[[ inputs.test_boolean ]]
+ parallel: $[[ inputs.test_number ]]
+ YAML
+ end
+
+ it 'loads and interpolates CI config YAML' do
+ expected_config = { test_job: { allow_failure: true, parallel: 8 } }
+
+ expect(result).to be_valid
+ expect(result).to be_interpolated
+ expect(result.content).to eq(expected_config)
+ end
+ end
+
+ context 'when interpolating and expanding variables' do
+ let(:inputs) { { test_input: '$TEST_VAR' } }
+
+ let(:variables) do
+ Gitlab::Ci::Variables::Collection.new([
+ { key: 'TEST_VAR', value: 'test variable', masked: false }
+ ])
+ end
+
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ ---
+ "test_job":
+ script:
+ - echo "$[[ inputs.test_input | expand_vars ]]"
+ YAML
+ end
+
+ it 'loads and interpolates CI config YAML' do
+ expected_config = { test_job: { script: ['echo "test variable"'] } }
+
+ expect(result).to be_valid
+ expect(result).to be_interpolated
+ expect(result.content).to eq(expected_config)
+ end
+ end
+
+ context 'when using !reference' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ job_name:
+ default: .example_ref
+ ---
+ .example_ref:
+ script:
+ - echo "$[[ inputs.test_input ]]"
+ rules:
+ - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
+
+ build_job:
+ script: echo "build"
+ rules:
+ - !reference ["$[[ inputs.job_name ]]", "rules"]
+
+ test_job:
+ script:
+ - !reference [.example_ref, script]
+ YAML
+ end
+
+ it 'loads and interpolates CI config YAML' do
+ expect(result).to be_valid
+ expect(result).to be_interpolated
+ expect(result.content).to include('.example_ref': {
+ rules: [{ if: '$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH' }],
+ script: ['echo "hello test"']
+ })
+ expect(result.content.dig(:build_job, :rules).first.data[:seq]).to eq(['.example_ref', 'rules'])
+ expect(result.content).to include(
+ test_job: { script: [an_instance_of(::Gitlab::Ci::Config::Yaml::Tags::Reference)] }
+ )
+ end
+ end
+
+ context 'when there are too many interpolation blocks' do
+ let(:inputs) { { first_input: 'first', second_input: 'second' } }
+
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ first_input:
+ second_input:
+ ---
+ test_job:
+ script:
+ - echo "$[[ inputs.first_input ]]"
+ - echo "$[[ inputs.second_input ]]"
+ YAML
+ end
+
+ it 'returns an error result' do
+ stub_const('::Gitlab::Ci::Config::Interpolation::TextTemplate::MAX_BLOCKS', 1)
+
+ expect(result).not_to be_valid
+ expect(result.error).to eq('too many interpolation blocks')
+ end
+ end
+
+ context 'when a block is invalid' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ ---
+ test_job:
+ script:
+ - echo "$[[ inputs.test_input | expand_vars | truncate(0,1) ]]"
+ YAML
+ end
+
+ it 'returns an error result' do
+ stub_const('::Gitlab::Ci::Config::Interpolation::Block::MAX_FUNCTIONS', 1)
+
+ expect(result).not_to be_valid
+ expect(result.error).to eq('too many functions in interpolation block')
+ end
+ end
+
+ context 'when the YAML file is too large' do
+ it 'returns an error result' do
+ stub_application_setting(ci_max_total_yaml_size_bytes: 1)
+
+ expect(result).not_to be_valid
+ expect(result.error).to eq('config too large')
+ end
+ end
+
+ context 'when given an empty YAML file' do
+ let(:inputs) { {} }
+ let(:yaml) { '' }
+
+ it 'returns an error result' do
+ expect(result).not_to be_valid
+ expect(result.error).to eq('Invalid configuration format')
+ end
+ end
+
+ context 'when ci_text_interpolation is disabled' do
+ before do
+ stub_feature_flags(ci_text_interpolation: false)
+ end
+
+ it 'loads and interpolates CI config YAML' do
+ expected_config = { test_job: { script: ['echo "hello test"'] } }
+
+ expect(result).to be_valid
+ expect(result).to be_interpolated
+ expect(result.content).to eq(expected_config)
+ end
+
+ context 'when hash interpolation fails' do
+ let(:yaml) do
+ <<~YAML
+ ---
+ spec:
+ inputs:
+ test_input:
+ ---
+ test_job:
+ script:
+ - echo "$[[ inputs.test_input | expand_vars | truncate(0,1) ]]"
+ YAML
+ end
+
+ it 'returns an error result' do
+ stub_const('::Gitlab::Ci::Config::Interpolation::Block::MAX_FUNCTIONS', 1)
+
+ expect(result).not_to be_valid
+ expect(result.error).to eq('interpolation interrupted by errors, too many functions in interpolation block')
+ end
+ end
+ end
end
describe '#load_uninterpolated_yaml' do
diff --git a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
index 4017076d29f..967cd1693a9 100644
--- a/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
+++ b/spec/lib/gitlab/ci/pipeline/chain/validate/external_spec.rb
@@ -56,6 +56,7 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External, feature_category
let(:validation_service_url) { 'https://validation-service.external/' }
before do
+ stub_feature_flags(external_pipeline_validation_migration: false)
stub_env('EXTERNAL_VALIDATION_SERVICE_URL', validation_service_url)
allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('correlation-id')
end
@@ -84,6 +85,42 @@ RSpec.describe Gitlab::Ci::Pipeline::Chain::Validate::External, feature_category
end
end
+ context 'when :external_pipeline_validation_migration feature flag is enabled' do
+ let(:migrated_validation_service_url) { 'https://runway.validation-service.external/' }
+
+ before do
+ stub_feature_flags(external_pipeline_validation_migration: project)
+ end
+
+ context 'when EXTERNAL_VALIDATION_SERVICE_RUNWAY_URL is NOT present' do
+ before do
+ stub_env('EXTERNAL_VALIDATION_SERVICE_RUNWAY_URL', nil)
+ end
+
+ it 'fallbacks to existing validation service URL' do
+ expect(::Gitlab::HTTP).to receive(:post) do |url, _params|
+ expect(url).to eq(validation_service_url)
+ end
+
+ perform!
+ end
+ end
+
+ context 'when EXTERNAL_VALIDATION_SERVICE_RUNWAY_URL is present' do
+ before do
+ stub_env('EXTERNAL_VALIDATION_SERVICE_RUNWAY_URL', migrated_validation_service_url)
+ end
+
+ it 'uses migrated validation service URL' do
+ expect(::Gitlab::HTTP).to receive(:post) do |url, _params|
+ expect(url).to eq(migrated_validation_service_url)
+ end
+
+ perform!
+ end
+ end
+ end
+
it 'respects the defined payload schema' do
expect(::Gitlab::HTTP).to receive(:post) do |_url, params|
expect(params[:body]).to match_schema('/external_validation')
diff --git a/spec/lib/gitlab/database/query_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzer_spec.rb
index 0b849063562..20599bb89b6 100644
--- a/spec/lib/gitlab/database/query_analyzer_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzer_spec.rb
@@ -3,6 +3,8 @@
require 'spec_helper'
RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do
+ using RSpec::Parameterized::TableSyntax
+
let(:analyzer) { double(:query_analyzer) }
let(:user_analyzer) { double(:user_query_analyzer) }
let(:disabled_analyzer) { double(:disabled_query_analyzer) }
@@ -181,12 +183,34 @@ RSpec.describe Gitlab::Database::QueryAnalyzer, query_analyzers: false do
expect { process_sql("SELECT 1 FROM projects") }.not_to raise_error
end
- def process_sql(sql)
+ def process_sql(sql, event_name = 'load')
described_class.instance.within do
ApplicationRecord.load_balancer.read_write do |connection|
- described_class.instance.send(:process_sql, sql, connection)
+ described_class.instance.send(:process_sql, sql, connection, event_name)
end
end
end
end
+
+ describe '#normalize_event_name' do
+ where(:event, :parsed_event) do
+ 'Project Load' | 'load'
+ 'Namespaces::UserNamespace Create' | 'create'
+ 'Project Update' | 'update'
+ 'Project Destroy' | 'destroy'
+ 'Project Pluck' | 'pluck'
+ 'Project Insert' | 'insert'
+ 'Project Delete All' | 'delete_all'
+ 'Project Exists?' | 'exists?'
+ nil | ''
+ 'TRANSACTION' | 'transaction'
+ 'SCHEMA' | 'schema'
+ end
+
+ with_them do
+ it 'parses event name correctly' do
+ expect(described_class.instance.send(:normalize_event_name, event)).to eq(parsed_event)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb
index 0fe19041b6d..0c1c694a3e3 100644
--- a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_id_analyzer_spec.rb
@@ -115,7 +115,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningIdAnalyzer, que
def process_sql(model, sql)
Gitlab::Database::QueryAnalyzer.instance.within do
# Skip load balancer and retrieve connection assigned to model
- Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection, 'load')
end
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb
index 1f86c2ccbb0..8b053fa0291 100644
--- a/spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/ci/partitioning_routing_analyzer_spec.rb
@@ -64,7 +64,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::Ci::PartitioningRoutingAnalyzer
def process_sql(model, sql)
Gitlab::Database::QueryAnalyzer.instance.within do
# Skip load balancer and retrieve connection assigned to model
- Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection, 'load')
end
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
index 1909e134e66..fb00fbe27ba 100644
--- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_metrics_spec.rb
@@ -99,7 +99,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasMetrics, query_ana
def process_sql(model, sql)
Gitlab::Database::QueryAnalyzer.instance.within do
# Skip load balancer and retrieve connection assigned to model
- Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection, 'load')
end
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
index 0664508fa8d..6a36db1870a 100644
--- a/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/gitlab_schemas_validate_connection_spec.rb
@@ -97,7 +97,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::GitlabSchemasValidateConnection
def process_sql(model, sql)
Gitlab::Database::QueryAnalyzer.instance.within([analyzer]) do
# Skip load balancer and retrieve connection assigned to model
- Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection, 'load')
end
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/log_large_in_lists_spec.rb b/spec/lib/gitlab/database/query_analyzers/log_large_in_lists_spec.rb
new file mode 100644
index 00000000000..5646c3ff3b6
--- /dev/null
+++ b/spec/lib/gitlab/database/query_analyzers/log_large_in_lists_spec.rb
@@ -0,0 +1,148 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe Gitlab::Database::QueryAnalyzers::LogLargeInLists, query_analyzers: false, feature_category: :database do
+ let(:analyzer) { described_class }
+ let(:fixture) { fixture_file("gitlab/database/query_analyzers/#{file}") }
+ let(:sql) { fixture.gsub('%IN_LIST%', arguments) }
+
+ # Reduce the in list size to 5 to help with testing
+ # Reduce the min query size to 50 to help with testing
+ before do
+ stub_const("#{described_class}::IN_SIZE_LIMIT", 5)
+ stub_const("#{described_class}::MIN_QUERY_SIZE", 50)
+ allow(analyzer).to receive(:backtrace).and_return([])
+ allow(analyzer).to receive(:suppressed?).and_return(true) # bypass suppressed? method to avoid false positives
+ end
+
+ after do
+ # Clears analyzers list after each test to reload the state of `enabled?` method
+ Thread.current[:query_analyzer_enabled_analyzers] = []
+ end
+
+ context 'when feature flag is enabled' do
+ before do
+ stub_feature_flags(log_large_in_list_queries: true)
+ Gitlab::Database::QueryAnalyzer.instance.begin!([analyzer])
+ end
+
+ context 'when conditions are satisfied for logging' do
+ where(:file, :arguments, :result, :event_name) do
+ [
+ [
+ 'small_query_with_in_list.txt',
+ '1, 2, 3, 4, 5, 6',
+ { message: 'large_in_list_found', matches: 1, in_list_size: "6", stacktrace: [], event_name: 'load' },
+ 'load'
+ ],
+ [
+ 'small_query_with_in_list.txt',
+ '1,2,3,4,5,6',
+ { message: 'large_in_list_found', matches: 1, in_list_size: "6", stacktrace: [], event_name: 'pluck' },
+ 'pluck'
+ ],
+ [
+ 'small_query_with_in_list.txt',
+ 'SELECT id FROM projects where id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)',
+ { message: 'large_in_list_found', matches: 1, in_list_size: "10", stacktrace: [], event_name: 'load' },
+ 'load'
+ ],
+ [
+ 'large_query_with_in_list.txt',
+ '1,2,3,4,5,6',
+ { message: 'large_in_list_found', matches: 1, in_list_size: "6", stacktrace: [], event_name: 'load' },
+ 'load'
+ ],
+ [
+ 'large_query_with_in_list.txt',
+ '1, 2, 3, 4, 5, 6',
+ { message: 'large_in_list_found', matches: 1, in_list_size: "6", stacktrace: [], event_name: 'pluck' },
+ 'pluck'
+ ],
+ [
+ 'large_query_with_in_list.txt',
+ 'SELECT id FROM projects where id IN (1, 2, 3, 4, 5, 6, 7, 8, 9, 10)',
+ { message: 'large_in_list_found', matches: 1, in_list_size: "10", stacktrace: [], event_name: 'load' },
+ 'load'
+ ]
+ ]
+ end
+
+ with_them do
+ it 'logs all the occurrences' do
+ expect(Gitlab::AppLogger).to receive(:warn).with(result)
+
+ process_sql(sql, event_name)
+ end
+ end
+ end
+
+ context 'when conditions are not satisfied for logging' do
+ where(:file, :arguments, :event_name) do
+ [
+ ['small_query_with_in_list.txt', '1, 2, 3, 4, 5', 'load'],
+ ['small_query_with_in_list.txt', '$1, $2, $3, $4, $5', 'load'],
+ ['small_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (1, 2, 3, 4, 5)', 'load'],
+ ['small_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (SELECT id FROM namespaces)', 'load'],
+ ['small_query_with_in_list.txt', '1, 2, 3, 4, 5', 'schema'],
+ ['large_query_with_in_list.txt', '1, 2, 3, 4, 5', 'load'],
+ ['large_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (1, 2, 3, 4, 5)', 'load'],
+ ['large_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (SELECT id FROM namespaces)', 'load'],
+ ['large_query_with_in_list.txt', '1, 2, 3, 4, 5', 'schema'],
+ ['small_query_without_in_list.txt', '', 'load'],
+ ['small_query_without_in_list.txt', '', 'schema']
+ ]
+ end
+
+ with_them do
+ it 'skips logging the occurrences' do
+ expect(Gitlab::AppLogger).not_to receive(:warn)
+
+ process_sql(sql, event_name)
+ end
+ end
+ end
+ end
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(log_large_in_list_queries: false)
+ Gitlab::Database::QueryAnalyzer.instance.begin!([analyzer])
+ end
+
+ where(:file, :arguments, :event_name) do
+ [
+ ['small_query_with_in_list.txt', '1, 2, 3, 4, 5, 6', 'load'],
+ ['small_query_with_in_list.txt', '$1, $2, $3, $4, $5, $6', 'load'],
+ ['small_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (1, 2, 3, 4, 5, 6)', 'load'],
+ ['small_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (1, 2, 3, 4, 5, 6)', 'load'],
+ ['small_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (SELECT id FROM namespaces)', 'load'],
+ ['small_query_with_in_list.txt', '1, 2, 3, 4, 5, 6', 'schema'],
+ ['large_query_with_in_list.txt', '1, 2, 3, 4, 5, 6', 'load'],
+ ['large_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (1, 2, 3, 4, 5, 6, 7, 8)', 'load'],
+ ['large_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN ($1, $2, $3, $4, $5, $6, $7)', 'load'],
+ ['large_query_with_in_list.txt', 'SELECT id FROM projects WHERE id IN (SELECT id FROM namespaces)', 'load'],
+ ['large_query_with_in_list.txt', '1, 2, 3, 4, 5, 6', 'schema'],
+ ['small_query_without_in_list.txt', '', 'load'],
+ ['small_query_without_in_list.txt', '', 'schema']
+ ]
+ end
+
+ with_them do
+ it 'skips logging the occurrences' do
+ expect(Gitlab::AppLogger).not_to receive(:warn)
+
+ process_sql(sql, event_name)
+ end
+ end
+ end
+
+ private
+
+ def process_sql(sql, event_name)
+ Gitlab::Database::QueryAnalyzer.instance.within do
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, ActiveRecord::Base.connection, event_name)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb
index 7fcdc59b691..00b16faab01 100644
--- a/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/prevent_set_operator_mismatch_spec.rb
@@ -9,7 +9,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::PreventSetOperatorMismatch, que
def process_sql(sql, model = ApplicationRecord)
Gitlab::Database::QueryAnalyzer.instance.within([analyzer]) do
# Skip load balancer and retrieve connection assigned to model
- Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection, 'load')
end
end
diff --git a/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb b/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb
index b90f60e0301..8054743c9a9 100644
--- a/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb
+++ b/spec/lib/gitlab/database/query_analyzers/restrict_allowed_schemas_spec.rb
@@ -180,7 +180,7 @@ RSpec.describe Gitlab::Database::QueryAnalyzers::RestrictAllowedSchemas,
yield if block_given?
# Skip load balancer and retrieve connection assigned to model
- Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection)
+ Gitlab::Database::QueryAnalyzer.instance.send(:process_sql, sql, model.retrieve_connection, 'load')
end
end
end
diff --git a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
index 4184c674823..844c3b54587 100644
--- a/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
+++ b/spec/lib/gitlab/metrics/exporter/base_exporter_spec.rb
@@ -167,9 +167,9 @@ RSpec.describe Gitlab::Metrics::Exporter::BaseExporter, feature_category: :cloud
describe '#start' do
it "doesn't start running server" do
- expect_any_instance_of(::WEBrick::HTTPServer).not_to receive(:start)
+ expect(::WEBrick::HTTPServer).not_to receive(:new)
- expect { exporter.start }.not_to change { exporter.thread? }
+ exporter.start
end
end
diff --git a/spec/models/packages/protection/rule_spec.rb b/spec/models/packages/protection/rule_spec.rb
index 03d0440f0d9..995f0035879 100644
--- a/spec/models/packages/protection/rule_spec.rb
+++ b/spec/models/packages/protection/rule_spec.rb
@@ -187,7 +187,7 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
end
end
- describe '.push_protected_from?' do
+ describe '.for_push_exists?' do
let_it_be(:project_with_ppr) { create(:project) }
let_it_be(:project_without_ppr) { create(:project) }
@@ -230,7 +230,7 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
subject do
project
.package_protection_rules
- .push_protected_from?(
+ .for_push_exists?(
access_level: access_level,
package_name: package_name,
package_type: package_type
@@ -270,8 +270,11 @@ RSpec.describe Packages::Protection::Rule, type: :model, feature_category: :pack
ref(:project_with_ppr) | Gitlab::Access::NO_ACCESS | '@my-scope/my-package-prod' | :npm | true
# Edge cases
- ref(:project_with_ppr) | 0 | '' | nil | true
- ref(:project_with_ppr) | nil | nil | nil | true
+ ref(:project_with_ppr) | nil | '@my-scope/my-package-stage-sha-1234' | :npm | false
+ ref(:project_with_ppr) | :developer | nil | :npm | false
+ ref(:project_with_ppr) | :developer | '' | :npm | false
+ ref(:project_with_ppr) | :developer | '@my-scope/my-package-stage-sha-1234' | nil | false
+ ref(:project_with_ppr) | nil | nil | nil | false
# For projects that have no package protection rules
ref(:project_without_ppr) | :developer | '@my-scope/my-package-prod' | :npm | false
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 7dea50ba270..8e4e90ae962 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1838,7 +1838,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors)
- .to include 'interpolation interrupted by errors, unknown interpolation key: `suite`'
+ .to include 'unknown interpolation key: `suite`'
end
end
@@ -2001,7 +2001,7 @@ RSpec.describe Ci::CreatePipelineService, :yaml_processor_feature_flag_corectnes
expect(pipeline).to be_persisted
expect(pipeline.yaml_errors)
- .to include 'interpolation interrupted by errors, unknown interpolation key: `suite`'
+ .to include 'unknown interpolation key: `suite`'
end
end
diff --git a/spec/support/rspec_order_todo.yml b/spec/support/rspec_order_todo.yml
index 872523e8d16..4be24d43363 100644
--- a/spec/support/rspec_order_todo.yml
+++ b/spec/support/rspec_order_todo.yml
@@ -5314,7 +5314,6 @@
- './spec/lib/gitlab/background_migration/backfill_namespace_id_of_vulnerability_reads_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_note_discussion_id_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_project_feature_package_registry_access_level_spec.rb'
-- './spec/lib/gitlab/background_migration/backfill_project_import_level_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_project_repositories_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_snippet_repositories_spec.rb'
- './spec/lib/gitlab/background_migration/backfill_topics_title_spec.rb'