summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2021-11-18 13:16:36 +0000
commit311b0269b4eb9839fa63f80c8d7a58f32b8138a0 (patch)
tree07e7870bca8aed6d61fdcc810731c50d2c40af47 /qa
parent27909cef6c4170ed9205afa7426b8d3de47cbb0c (diff)
downloadgitlab-ce-311b0269b4eb9839fa63f80c8d7a58f32b8138a0.tar.gz
Add latest changes from gitlab-org/gitlab@14-5-stable-eev14.5.0-rc42
Diffstat (limited to 'qa')
-rw-r--r--qa/Gemfile8
-rw-r--r--qa/Gemfile.lock68
-rw-r--r--qa/Rakefile1
-rw-r--r--qa/chemlab-library-gitlab.gemspec1
-rw-r--r--qa/lib/gitlab.rb29
-rw-r--r--qa/lib/gitlab/page/admin/subscription.rb9
-rw-r--r--qa/lib/gitlab/page/admin/subscription.stub.rb216
-rw-r--r--qa/lib/gitlab/page/main/welcome.rb13
-rw-r--r--qa/lib/gitlab/page/main/welcome.stub.rb33
-rw-r--r--qa/qa.rb3
-rw-r--r--qa/qa/fixtures/kubernetes_agent/agentk-manifest.yaml.erb3
-rw-r--r--qa/qa/fixtures/rubygems_package/mygem.gemspec39
-rw-r--r--qa/qa/flow/login.rb7
-rw-r--r--qa/qa/mobile/page/main/menu.rb60
-rw-r--r--qa/qa/mobile/page/profile/menu.rb26
-rw-r--r--qa/qa/mobile/page/project/issue/show.rb37
-rw-r--r--qa/qa/mobile/page/project/show.rb31
-rw-r--r--qa/qa/mobile/page/sub_menus/common.rb28
-rw-r--r--qa/qa/page/base.rb29
-rw-r--r--qa/qa/page/component/issuable/sidebar.rb78
-rw-r--r--qa/qa/page/component/issue_board/show.rb19
-rw-r--r--qa/qa/page/group/bulk_import.rb2
-rw-r--r--qa/qa/page/group/settings/package_registries.rb22
-rw-r--r--qa/qa/page/group/show.rb5
-rw-r--r--qa/qa/page/main/login.rb6
-rw-r--r--qa/qa/page/main/menu.rb18
-rw-r--r--qa/qa/page/merge_request/show.rb11
-rw-r--r--qa/qa/page/profile/menu.rb4
-rw-r--r--qa/qa/page/project/fork/new.rb2
-rw-r--r--qa/qa/page/project/import/github.rb7
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/add.rb2
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/index.rb8
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/show.rb46
-rw-r--r--qa/qa/page/project/issue/show.rb5
-rw-r--r--qa/qa/page/project/job/show.rb6
-rw-r--r--qa/qa/page/project/monitor/metrics/show.rb1
-rw-r--r--qa/qa/page/project/new.rb8
-rw-r--r--qa/qa/page/project/registry/show.rb6
-rw-r--r--qa/qa/page/project/settings/deploy_tokens.rb12
-rw-r--r--qa/qa/page/project/show.rb7
-rw-r--r--qa/qa/page/project/sub_menus/common.rb4
-rw-r--r--qa/qa/page/sub_menus/common.rb4
-rw-r--r--qa/qa/resource/base.rb11
-rw-r--r--qa/qa/resource/bulk_import_group.rb12
-rw-r--r--qa/qa/resource/clusters/agent.rb13
-rw-r--r--qa/qa/resource/clusters/agent_token.rb13
-rw-r--r--qa/qa/resource/deploy_token.rb3
-rw-r--r--qa/qa/resource/file.rb2
-rw-r--r--qa/qa/resource/fork.rb8
-rw-r--r--qa/qa/resource/issue.rb47
-rw-r--r--qa/qa/resource/kubernetes_cluster/project_cluster.rb14
-rw-r--r--qa/qa/resource/merge_request_from_fork.rb2
-rw-r--r--qa/qa/resource/project.rb18
-rw-r--r--qa/qa/resource/sandbox.rb16
-rw-r--r--qa/qa/runtime/allure_report.rb9
-rw-r--r--qa/qa/runtime/browser.rb5
-rw-r--r--qa/qa/runtime/env.rb13
-rw-r--r--qa/qa/runtime/feature.rb38
-rw-r--r--qa/qa/scenario/bootable.rb16
-rw-r--r--qa/qa/scenario/shared_attributes.rb3
-rw-r--r--qa/qa/scenario/template.rb2
-rw-r--r--qa/qa/scenario/test/instance/reliable.rb16
-rw-r--r--qa/qa/scenario/test/integration/ldap_no_tls.rb3
-rw-r--r--qa/qa/scenario/test/integration/ldap_tls.rb3
-rw-r--r--qa/qa/scenario/test/integration/registry_tls.rb13
-rw-r--r--qa/qa/service/cluster_provider/gcloud.rb31
-rw-r--r--qa/qa/service/kubernetes_cluster.rb13
-rw-r--r--qa/qa/service/praefect_manager.rb94
-rw-r--r--qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb18
-rw-r--r--qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb71
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb2
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb15
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb10
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb62
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb27
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb8
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb32
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb191
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/container_registry_spec.rb)0
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/container_registry/online_garbage_collection_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb)13
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb88
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb)21
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb340
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/composer_registry_spec.rb)0
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/conan_repository_spec.rb)2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/generic_repository_spec.rb)6
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/helm_registry_spec.rb)2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb)2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb317
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb)31
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb197
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/nuget_repository_spec.rb)2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb)6
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/rubygems_registry_spec.rb)62
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb62
-rw-r--r--qa/qa/specs/helpers/context_selector.rb4
-rw-r--r--qa/qa/support/fabrication_tracker.rb53
-rw-r--r--qa/qa/support/formatters/test_stats_formatter.rb13
-rw-r--r--qa/qa/support/helpers/plan.rb66
-rw-r--r--qa/qa/support/matchers/eventually_matcher.rb6
-rw-r--r--qa/qa/support/matchers/have_matcher.rb1
-rw-r--r--qa/qa/support/repeater.rb43
-rw-r--r--qa/qa/support/retrier.rb34
-rw-r--r--qa/qa/support/waiter.rb23
-rw-r--r--qa/qa/tools/reliable_report.rb234
-rw-r--r--qa/spec/runtime/feature_spec.rb48
-rw-r--r--qa/spec/scenario/test/instance/reliable_spec.rb7
-rw-r--r--qa/spec/spec_helper.rb12
-rw-r--r--qa/spec/specs/allure_report_spec.rb19
-rw-r--r--qa/spec/specs/helpers/context_selector_spec.rb18
-rw-r--r--qa/spec/support/formatters/test_stats_formatter_spec.rb26
-rw-r--r--qa/spec/support/repeater_spec.rb114
-rw-r--r--qa/spec/support/retrier_spec.rb71
-rw-r--r--qa/spec/support/shared_contexts/packages_registry_shared_context.rb7
-rw-r--r--qa/spec/support/waiter_spec.rb35
-rw-r--r--qa/spec/tools/reliable_report_spec.rb145
-rw-r--r--qa/tasks/reliable_report.rake21
125 files changed, 2864 insertions, 1097 deletions
diff --git a/qa/Gemfile b/qa/Gemfile
index ee90d049d7b..498d05b2254 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -7,9 +7,9 @@ gem 'activesupport', '~> 6.1.4.1' # This should stay in sync with the root's Gem
gem 'allure-rspec', '~> 2.15.0'
gem 'capybara', '~> 3.35.0'
gem 'capybara-screenshot', '~> 1.0.23'
-gem 'rake', '~> 12.3.3'
+gem 'rake', '~> 13'
gem 'rspec', '~> 3.10'
-gem 'selenium-webdriver', '~> 4.0.0.rc1'
+gem 'selenium-webdriver', '~> 4.0'
gem 'airborne', '~> 0.3.4', require: false # airborne is messing with rspec sandboxed mode so not requiring by default
gem 'rest-client', '~> 2.1.0'
gem 'rspec-retry', '~> 0.6.1', require: 'rspec/retry'
@@ -22,9 +22,11 @@ gem 'timecop', '~> 0.9.1'
gem 'parallel', '~> 1.19'
gem 'rspec-parameterized', '~> 0.4.2'
gem 'octokit', '~> 4.21'
-gem 'webdrivers', '~> 4.6'
+gem 'webdrivers', '~> 5.0'
gem 'zeitwerk', '~> 2.4'
gem 'influxdb-client', '~> 1.17'
+gem 'terminal-table', '~> 1.8', require: false
+gem 'slack-notifier', '~> 2.4', require: false
gem 'chemlab', '~> 0.9'
gem 'chemlab-library-www-gitlab-com', '~> 0.1'
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 153a141d3fd..2b5b5e368cf 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -11,7 +11,7 @@ GEM
adamantium (0.2.0)
ice_nine (~> 0.11.0)
memoizable (~> 0.4.0)
- addressable (2.7.0)
+ addressable (2.8.0)
public_suffix (>= 2.0.2, < 5.0)
airborne (0.3.4)
activesupport
@@ -27,7 +27,7 @@ GEM
oj (>= 3.10, < 4)
require_all (>= 2, < 4)
uuid (>= 2.3, < 3)
- ast (2.4.1)
+ ast (2.4.2)
binding_ninja (0.2.3)
byebug (9.1.0)
capybara (3.35.3)
@@ -41,7 +41,7 @@ GEM
capybara-screenshot (1.0.23)
capybara (>= 1.0, < 4)
launchy
- chemlab (0.9.1)
+ chemlab (0.9.2)
colorize (~> 0.8)
i18n (~> 1.8)
rake (>= 12, < 14)
@@ -88,27 +88,25 @@ GEM
gitlab (4.16.1)
httparty (~> 0.14, >= 0.14.0)
terminal-table (~> 1.5, >= 1.5.1)
- gitlab-qa (7.9.1)
+ gitlab-qa (7.14.0)
activesupport (~> 6.1)
gitlab (~> 4.16.1)
- http (= 4.3.0)
+ http (~> 5.0)
nokogiri (~> 1.10)
table_print (= 1.5.7)
- http (4.3.0)
- addressable (~> 2.3)
+ http (5.0.4)
+ addressable (~> 2.8)
http-cookie (~> 1.0)
http-form_data (~> 2.2)
- http-parser (~> 1.2.0)
+ llhttp-ffi (~> 0.4.0)
http-accept (1.7.0)
- http-cookie (1.0.3)
+ http-cookie (1.0.4)
domain_name (~> 0.5)
http-form_data (2.3.0)
- http-parser (1.2.3)
- ffi-compiler (>= 1.0, < 2.0)
- httparty (0.19.0)
+ httparty (0.20.0)
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
- i18n (1.8.10)
+ i18n (1.8.11)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
influxdb-client (1.17.0)
@@ -116,22 +114,25 @@ GEM
rake
launchy (2.4.3)
addressable (~> 2.3)
+ llhttp-ffi (0.4.0)
+ ffi-compiler (~> 1.0)
+ rake (~> 13.0)
macaddr (1.7.2)
systemu (~> 2.6.5)
memoizable (0.4.2)
thread_safe (~> 0.3, >= 0.3.1)
method_source (0.9.0)
- mime-types (3.3.1)
+ mime-types (3.4.0)
mime-types-data (~> 3.2015)
- mime-types-data (3.2021.0704)
+ mime-types-data (3.2021.1115)
mini_mime (1.1.0)
- mini_portile2 (2.5.3)
+ mini_portile2 (2.6.1)
minitest (5.14.4)
multi_xml (0.6.0)
multipart-post (2.1.1)
netrc (0.11.0)
- nokogiri (1.11.7)
- mini_portile2 (~> 2.5.0)
+ nokogiri (1.12.5)
+ mini_portile2 (~> 2.6.1)
racc (~> 1.4)
octokit (4.21.0)
faraday (>= 0.9)
@@ -140,7 +141,7 @@ GEM
parallel (1.19.2)
parallel_tests (2.29.0)
parallel
- parser (2.7.1.4)
+ parser (3.0.2.0)
ast (~> 2.4.1)
proc_to_ast (0.1.0)
coderay
@@ -153,12 +154,12 @@ GEM
pry-byebug (3.5.1)
byebug (~> 9.1)
pry (~> 0.10)
- public_suffix (4.0.1)
- racc (1.5.2)
+ public_suffix (4.0.6)
+ racc (1.6.0)
rack (2.2.3)
rack-test (1.1.0)
rack (>= 1.0, < 3)
- rake (12.3.3)
+ rake (13.0.6)
regexp_parser (2.1.1)
require_all (3.0.0)
rest-client (2.1.0)
@@ -198,10 +199,11 @@ GEM
sawyer (0.8.2)
addressable (>= 2.3.5)
faraday (> 0.8, < 2.0)
- selenium-webdriver (4.0.0.rc1)
+ selenium-webdriver (4.0.3)
childprocess (>= 0.5, < 5.0)
- rexml (~> 3.2)
+ rexml (~> 3.2, >= 3.2.5)
rubyzip (>= 1.2.2)
+ slack-notifier (2.4.0)
systemu (2.6.5)
table_print (1.5.7)
terminal-table (1.8.0)
@@ -212,7 +214,7 @@ GEM
concurrent-ruby (~> 1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.7.7)
+ unf_ext (0.0.8)
unicode-display_width (1.8.0)
unparser (0.4.7)
abstract_type (~> 0.0.7)
@@ -227,13 +229,13 @@ GEM
watir (6.19.1)
regexp_parser (>= 1.2, < 3)
selenium-webdriver (>= 3.142.7)
- webdrivers (4.6.0)
+ webdrivers (5.0.0)
nokogiri (~> 1.6)
rubyzip (>= 1.3.0)
- selenium-webdriver (>= 3.0, < 4.0)
+ selenium-webdriver (~> 4.0)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.4.2)
+ zeitwerk (2.5.1)
PLATFORMS
ruby
@@ -255,7 +257,7 @@ DEPENDENCIES
parallel (~> 1.19)
parallel_tests (~> 2.29)
pry-byebug (~> 3.5.1)
- rake (~> 12.3.3)
+ rake (~> 13)
rest-client (~> 2.1.0)
rotp (~> 3.1.0)
rspec (~> 3.10)
@@ -263,10 +265,12 @@ DEPENDENCIES
rspec-retry (~> 0.6.1)
rspec_junit_formatter (~> 0.4.1)
ruby-debug-ide (~> 0.7.0)
- selenium-webdriver (~> 4.0.0.rc1)
+ selenium-webdriver (~> 4.0)
+ slack-notifier (~> 2.4)
+ terminal-table (~> 1.8)
timecop (~> 0.9.1)
- webdrivers (~> 4.6)
+ webdrivers (~> 5.0)
zeitwerk (~> 2.4)
BUNDLED WITH
- 2.2.22
+ 2.2.30
diff --git a/qa/Rakefile b/qa/Rakefile
index f24c81a9ec2..57360e98ca2 100644
--- a/qa/Rakefile
+++ b/qa/Rakefile
@@ -2,6 +2,7 @@
# rubocop:disable Rails/RakeEnvironment
load 'tasks/webdrivers.rake'
+load 'tasks/reliable_report.rake'
require_relative 'qa/tools/revoke_all_personal_access_tokens'
require_relative 'qa/tools/delete_subgroups'
diff --git a/qa/chemlab-library-gitlab.gemspec b/qa/chemlab-library-gitlab.gemspec
index 34a55ba8927..9af4a650d98 100644
--- a/qa/chemlab-library-gitlab.gemspec
+++ b/qa/chemlab-library-gitlab.gemspec
@@ -19,4 +19,5 @@ Gem::Specification.new do |spec|
spec.require_paths = ['lib']
spec.add_runtime_dependency 'chemlab', '~> 0.9'
+ spec.add_runtime_dependency 'zeitwerk', '~> 2.4'
end
diff --git a/qa/lib/gitlab.rb b/qa/lib/gitlab.rb
index 4418e51facb..8c33071633d 100644
--- a/qa/lib/gitlab.rb
+++ b/qa/lib/gitlab.rb
@@ -1,31 +1,14 @@
# frozen_string_literal: true
require 'chemlab/library'
+require 'zeitwerk'
+
+loader = Zeitwerk::Loader.new
+loader.push_dir(__dir__)
+loader.ignore("#{__dir__}/gitlab/**/*.stub.rb") # ignore page stubs
+loader.setup
# Chemlab Page Libraries for GitLab
module Gitlab
include Chemlab::Library
-
- module Page
- module Main
- autoload :Login, 'gitlab/page/main/login'
- autoload :SignUp, 'gitlab/page/main/sign_up'
- end
-
- module Subscriptions
- autoload :New, 'gitlab/page/subscriptions/new'
- end
-
- module Admin
- autoload :Dashboard, 'gitlab/page/admin/dashboard'
- autoload :Subscription, 'gitlab/page/admin/subscription'
- end
-
- module Group
- module Settings
- autoload :Billing, 'gitlab/page/group/settings/billing'
- autoload :UsageQuotas, 'gitlab/page/group/settings/usage_quotas'
- end
- end
- end
end
diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb
index 0f7c6b4c211..cdd9bb20b42 100644
--- a/qa/lib/gitlab/page/admin/subscription.rb
+++ b/qa/lib/gitlab/page/admin/subscription.rb
@@ -6,7 +6,16 @@ module Gitlab
class Subscription < Chemlab::Page
path '/admin/subscription'
+ p :plan
+ p :started
+ p :name
+ p :company
+ p :email
+ h2 :billable_users
+ h2 :maximum_users
h2 :users_in_subscription
+ h2 :users_over_subscription
+ table :subscription_history
end
end
end
diff --git a/qa/lib/gitlab/page/admin/subscription.stub.rb b/qa/lib/gitlab/page/admin/subscription.stub.rb
index 51f23e7f0d0..89d7bfb95d9 100644
--- a/qa/lib/gitlab/page/admin/subscription.stub.rb
+++ b/qa/lib/gitlab/page/admin/subscription.stub.rb
@@ -4,6 +4,174 @@ module Gitlab
module Page
module Admin
module Subscription
+ # @note Defined as +p :plan+
+ # @return [String] The text content or value of +plan+
+ def plan
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.plan_element).to exist
+ # end
+ # @return [Watir::P] The raw +P+ element
+ def plan_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_plan
+ # end
+ # @return [Boolean] true if the +plan+ element is present on the page
+ def plan?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +p :started+
+ # @return [String] The text content or value of +started+
+ def started
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.started_element).to exist
+ # end
+ # @return [Watir::P] The raw +P+ element
+ def started_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_started
+ # end
+ # @return [Boolean] true if the +started+ element is present on the page
+ def started?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +p :name+
+ # @return [String] The text content or value of +name+
+ def name
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.name_element).to exist
+ # end
+ # @return [Watir::P] The raw +P+ element
+ def name_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_name
+ # end
+ # @return [Boolean] true if the +name+ element is present on the page
+ def name?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +p :company+
+ # @return [String] The text content or value of +company+
+ def company
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.company_element).to exist
+ # end
+ # @return [Watir::P] The raw +P+ element
+ def company_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_company
+ # end
+ # @return [Boolean] true if the +company+ element is present on the page
+ def company?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +p :email+
+ # @return [String] The text content or value of +email+
+ def email
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.email_element).to exist
+ # end
+ # @return [Watir::P] The raw +P+ element
+ def email_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_email
+ # end
+ # @return [Boolean] true if the +email+ element is present on the page
+ def email?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +h2 :billable_users+
+ # @return [String] The text content or value of +billable_users+
+ def billable_users
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.billable_users_element).to exist
+ # end
+ # @return [Watir::H2] The raw +H2+ element
+ def billable_users_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_billable_users
+ # end
+ # @return [Boolean] true if the +billable_users+ element is present on the page
+ def billable_users?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +h2 :maximum_users+
+ # @return [String] The text content or value of +maximum_users+
+ def maximum_users
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.maximum_users_element).to exist
+ # end
+ # @return [Watir::H2] The raw +H2+ element
+ def maximum_users_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_maximum_users
+ # end
+ # @return [Boolean] true if the +maximum_users+ element is present on the page
+ def maximum_users?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
# @note Defined as +h2 :users_in_subscription+
# @return [String] The text content or value of +users_in_subscription+
def users_in_subscription
@@ -27,6 +195,54 @@ module Gitlab
def users_in_subscription?
# This is a stub, used for indexing. The method is dynamically generated.
end
+
+ # @note Defined as +h2 :users_over_subscription+
+ # @return [String] The text content or value of +users_over_subscription+
+ def users_over_subscription
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.users_over_subscription_element).to exist
+ # end
+ # @return [Watir::H2] The raw +H2+ element
+ def users_over_subscription_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_users_over_subscription
+ # end
+ # @return [Boolean] true if the +users_over_subscription+ element is present on the page
+ def users_over_subscription?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @note Defined as +table :subscription_history+
+ # @return [String] The text content or value of +subscription_history+
+ def subscription_history
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription.subscription_history_element).to exist
+ # end
+ # @return [Watir::Table] The raw +Table+ element
+ def subscription_history_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Admin::Subscription.perform do |subscription|
+ # expect(subscription).to be_subscription_history
+ # end
+ # @return [Boolean] true if the +subscription_history+ element is present on the page
+ def subscription_history?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
end
end
end
diff --git a/qa/lib/gitlab/page/main/welcome.rb b/qa/lib/gitlab/page/main/welcome.rb
new file mode 100644
index 00000000000..a2df1da61c9
--- /dev/null
+++ b/qa/lib/gitlab/page/main/welcome.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Main
+ class Welcome < Chemlab::Page
+ path '/users/sign_up/welcome'
+
+ button :get_started_button
+ end
+ end
+ end
+end
diff --git a/qa/lib/gitlab/page/main/welcome.stub.rb b/qa/lib/gitlab/page/main/welcome.stub.rb
new file mode 100644
index 00000000000..a10e697bcbf
--- /dev/null
+++ b/qa/lib/gitlab/page/main/welcome.stub.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module Page
+ module Main
+ module Welcome
+ # @note Defined as +button :get_started_button+
+ # Clicks +get_started_button+
+ def get_started_button
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Welcome.perform do |welcome|
+ # expect(welcome.get_started_button_element).to exist
+ # end
+ # @return [Watir::Button] The raw +Button+ element
+ def get_started_button_element
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+
+ # @example
+ # Gitlab::Page::Main::Welcome.perform do |welcome|
+ # expect(welcome).to be_get_started_button
+ # end
+ # @return [Boolean] true if the +get_started_button+ element is present on the page
+ def get_started_button?
+ # This is a stub, used for indexing. The method is dynamically generated.
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa.rb b/qa/qa.rb
index cc83efb90e8..1cdf6bc89ca 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -50,7 +50,8 @@ module QA
"user_gpg" => "UserGPG",
"smtp" => "SMTP",
"otp" => "OTP",
- "jira_api" => "JiraAPI"
+ "jira_api" => "JiraAPI",
+ "registry_tls" => "RegistryTLS"
)
loader.setup
diff --git a/qa/qa/fixtures/kubernetes_agent/agentk-manifest.yaml.erb b/qa/qa/fixtures/kubernetes_agent/agentk-manifest.yaml.erb
index a13c92d5c6d..8eac8419022 100644
--- a/qa/qa/fixtures/kubernetes_agent/agentk-manifest.yaml.erb
+++ b/qa/qa/fixtures/kubernetes_agent/agentk-manifest.yaml.erb
@@ -24,8 +24,7 @@ spec:
args:
- --token-file=/config/token
- --kas-address
- - "<%= kas_wss_address %>" # Use this for GitLab chart deployments
- # - "<%= kas_grpc_address %>" # Use this for GDK
+ - "<%= kas_wss_address %>"
volumeMounts:
- name: token-volume
mountPath: /config
diff --git a/qa/qa/fixtures/rubygems_package/mygem.gemspec b/qa/qa/fixtures/rubygems_package/mygem.gemspec
deleted file mode 100644
index 33d8c88e5ac..00000000000
--- a/qa/qa/fixtures/rubygems_package/mygem.gemspec
+++ /dev/null
@@ -1,39 +0,0 @@
-# frozen_string_literal: true
-
-Gem::Specification.new do |s|
- s.name = 'mygem'
- s.authors = ['Tanuki Steve', 'Hal 9000']
- s.author = 'Tanuki Steve'
- s.version = '0.0.1'
- s.date = '2011-09-29'
- s.summary = 'package is the best'
- s.files = ['lib/hello_gem.rb']
- s.require_paths = ['lib']
-
- s.description = 'A test package for GitLab.'
- s.email = 'tanuki@not_real.com'
- s.homepage = 'https://gitlab.com/ruby-co/my-package'
- s.license = 'MIT'
-
- s.metadata = {
- 'bug_tracker_uri' => 'https://gitlab.com/ruby-co/my-package/issues',
- 'changelog_uri' => 'https://gitlab.com/ruby-co/my-package/CHANGELOG.md',
- 'documentation_uri' => 'https://gitlab.com/ruby-co/my-package/docs',
- 'mailing_list_uri' => 'https://gitlab.com/ruby-co/my-package/mailme',
- 'source_code_uri' => 'https://gitlab.com/ruby-co/my-package'
- }
-
- s.bindir = 'bin'
- s.platform = Gem::Platform::RUBY
- s.post_install_message = 'Installed, thank you!'
- s.rdoc_options = ['--main']
- s.required_ruby_version = '>= 2.7.0'
- s.required_rubygems_version = '>= 1.8.11'
- s.requirements = 'A high powered server or calculator'
- s.rubygems_version = '1.8.09'
-
- s.add_dependency 'dependency_1', '~> 1.2.3'
- s.add_dependency 'dependency_2', '3.0.0'
- s.add_dependency 'dependency_3', '>= 1.0.0'
- s.add_dependency 'dependency_4'
-end
diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb
index 05a509588f1..5f7e0227ac5 100644
--- a/qa/qa/flow/login.rb
+++ b/qa/qa/flow/login.rb
@@ -23,8 +23,11 @@ module QA
end
def sign_in(as: nil, address: :gitlab, skip_page_validation: false, admin: false)
- Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform(&:signed_in?)
- Runtime::Browser.visit(address, Page::Main::Login)
+ unless Page::Main::Login.perform(&:on_login_page?)
+ Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform(&:signed_in?)
+ Runtime::Browser.visit(address, Page::Main::Login)
+ end
+
Page::Main::Login.perform do |login|
if admin
login.sign_in_using_admin_credentials
diff --git a/qa/qa/mobile/page/main/menu.rb b/qa/qa/mobile/page/main/menu.rb
new file mode 100644
index 00000000000..40bb421b383
--- /dev/null
+++ b/qa/qa/mobile/page/main/menu.rb
@@ -0,0 +1,60 @@
+# frozen_string_literal: true
+
+module QA
+ module Mobile
+ module Page
+ module Main
+ module Menu
+ extend QA::Page::PageConcern
+
+ def self.prepended(base)
+ super
+
+ base.class_eval do
+ view 'app/views/layouts/header/_default.html.haml' do
+ element :mobile_navbar_button, required: true
+ end
+
+ view 'app/assets/javascripts/nav/components/responsive_home.vue' do
+ element :mobile_new_dropdown
+ end
+ end
+ end
+
+ def open_mobile_menu
+ if has_no_element?(:user_avatar)
+ Support::Retrier.retry_until do
+ click_element(:mobile_navbar_button)
+ has_element?(:user_avatar)
+ end
+ end
+ end
+
+ def open_mobile_new_dropdown
+ open_mobile_menu
+
+ Support::Retrier.retry_until do
+ find('[data-qa-selector="mobile_new_dropdown"] > button').click
+ has_css?('.dropdown-menu-right.show')
+ end
+ end
+
+ def has_personal_area?(wait: Capybara.default_max_wait_time)
+ open_mobile_menu
+ super
+ end
+
+ def has_no_personal_area?(wait: Capybara.default_max_wait_time)
+ open_mobile_menu
+ super
+ end
+
+ def within_user_menu
+ open_mobile_menu
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/mobile/page/profile/menu.rb b/qa/qa/mobile/page/profile/menu.rb
new file mode 100644
index 00000000000..34c53a95e03
--- /dev/null
+++ b/qa/qa/mobile/page/profile/menu.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+module QA
+ module Mobile
+ module Page
+ module Profile
+ module Menu
+ extend QA::Page::PageConcern
+
+ def self.prepended(base)
+ super
+
+ base.class_eval do
+ prepend QA::Mobile::Page::Main::Menu
+ end
+ end
+
+ def within_sidebar
+ open_mobile_nav_sidebar
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/mobile/page/project/issue/show.rb b/qa/qa/mobile/page/project/issue/show.rb
new file mode 100644
index 00000000000..017ecebcb69
--- /dev/null
+++ b/qa/qa/mobile/page/project/issue/show.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module QA
+ module Mobile
+ module Page
+ module Project
+ module Issue
+ module Show
+ extend QA::Page::PageConcern
+
+ def self.prepended(base)
+ super
+
+ base.class_eval do
+ view 'app/assets/javascripts/issue_show/components/header_actions.vue' do
+ element :issue_actions_dropdown
+ element :mobile_close_issue_button
+ element :mobile_reopen_issue_button
+ end
+ end
+ end
+
+ def click_close_issue_button
+ find('[data-qa-selector="issue_actions_dropdown"] > button').click
+ find_element(:mobile_close_issue_button, visible: false).click
+ end
+
+ def has_reopen_issue_button?
+ find('[data-qa-selector="issue_actions_dropdown"] > button').click
+ has_element?(:mobile_reopen_issue_button)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/mobile/page/project/show.rb b/qa/qa/mobile/page/project/show.rb
new file mode 100644
index 00000000000..8a0a222c852
--- /dev/null
+++ b/qa/qa/mobile/page/project/show.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module QA
+ module Mobile
+ module Page
+ module Project
+ module Show
+ extend QA::Page::PageConcern
+
+ def self.prepended(base)
+ super
+
+ base.class_eval do
+ prepend QA::Mobile::Page::Main::Menu
+
+ view 'app/assets/javascripts/nav/components/top_nav_new_dropdown.vue' do
+ element :new_issue_mobile_button
+ end
+ end
+ end
+
+ def go_to_new_issue
+ open_mobile_new_dropdown
+
+ click_element(:new_issue_mobile_button)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/mobile/page/sub_menus/common.rb b/qa/qa/mobile/page/sub_menus/common.rb
new file mode 100644
index 00000000000..6a0477a3f31
--- /dev/null
+++ b/qa/qa/mobile/page/sub_menus/common.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module QA
+ module Mobile
+ module Page
+ module SubMenus
+ module Common
+ def open_mobile_nav_sidebar
+ if has_element?(:project_sidebar, visible: false)
+ Support::Retrier.retry_until do
+ click_element(:toggle_mobile_nav_button)
+ has_element?(:project_sidebar, visible: true)
+ end
+ end
+ end
+
+ def within_sidebar
+ wait_for_requests
+
+ open_mobile_nav_sidebar
+
+ super
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 9debdc1d4dd..4708063b2eb 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -57,16 +57,25 @@ module QA
end
end
- def retry_until(max_attempts: 3, reload: false, sleep_interval: 0, raise_on_failure: true)
- Support::Retrier.retry_until(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval, raise_on_failure: raise_on_failure) do
- yield
- end
- end
-
- def retry_on_exception(max_attempts: 3, reload: false, sleep_interval: 0.5)
- Support::Retrier.retry_on_exception(max_attempts: max_attempts, reload_page: (reload && self), sleep_interval: sleep_interval) do
- yield
- end
+ def retry_until(max_attempts: 3, reload: false, sleep_interval: 0, raise_on_failure: true, message: nil, &block)
+ Support::Retrier.retry_until(
+ max_attempts: max_attempts,
+ reload_page: (reload && self),
+ sleep_interval: sleep_interval,
+ raise_on_failure: raise_on_failure,
+ message: message,
+ &block
+ )
+ end
+
+ def retry_on_exception(max_attempts: 3, reload: false, sleep_interval: 0.5, message: nil, &block)
+ Support::Retrier.retry_on_exception(
+ max_attempts: max_attempts,
+ reload_page: (reload && self),
+ sleep_interval: sleep_interval,
+ message: message,
+ &block
+ )
end
def scroll_to(selector, text: nil)
diff --git a/qa/qa/page/component/issuable/sidebar.rb b/qa/qa/page/component/issuable/sidebar.rb
index 971e7634f6d..77962570aed 100644
--- a/qa/qa/page/component/issuable/sidebar.rb
+++ b/qa/qa/page/component/issuable/sidebar.rb
@@ -22,20 +22,16 @@ module QA
element :labels_block
end
- base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue' do
- element :selected_label_content
+ base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue' do
+ element :dropdown_input_field
end
- base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue' do
+ base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_contents.vue' do
element :labels_dropdown_content
end
- base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue' do
- element :labels_edit_button
- end
-
- base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue' do
- element :dropdown_input_field
+ base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_widget/dropdown_value.vue' do
+ element :selected_label_content
end
base.view 'app/views/shared/issuable/_sidebar.html.haml' do
@@ -53,7 +49,7 @@ module QA
end
def assign_milestone(milestone)
- within_element(:milestone_block) do
+ wait_milestone_block_finish_loading do
click_element(:edit_link)
click_on(milestone.title)
end
@@ -70,14 +66,14 @@ module QA
end
def has_assignee?(username)
- within_element(:assignee_block) do
- has_text?(username, wait: 1)
+ wait_assignees_block_finish_loading do
+ has_text?(username)
end
end
def has_no_assignee?(username)
- within_element(:assignee_block) do
- has_no_text?(username, wait: 1)
+ wait_assignees_block_finish_loading do
+ has_no_text?(username)
end
end
@@ -88,8 +84,14 @@ module QA
end
def has_label?(label)
- within_element(:labels_block) do
- !!has_element?(:selected_label_content, label_name: label)
+ wait_labels_block_finish_loading do
+ has_element?(:selected_label_content, label_name: label)
+ end
+ end
+
+ def has_no_label?(label)
+ wait_labels_block_finish_loading do
+ has_no_element?(:selected_label_content, label_name: label)
end
end
@@ -103,33 +105,34 @@ module QA
find_element(:more_assignees_link)
end
- def select_labels_and_refresh(labels)
- Support::Retrier.retry_until do
- click_element(:labels_edit_button)
- has_element?(:labels_dropdown_content, text: labels.first)
- end
+ def select_labels(labels)
+ within_element(:labels_block) do
+ click_element(:edit_link)
- labels.each do |label|
- within_element(:labels_dropdown_content) do
- send_keys_to_element(:dropdown_input_field, [label, :enter])
+ labels.each do |label|
+ within_element(:labels_dropdown_content) do
+ fill_element(:dropdown_input_field, label)
+ click_button(text: label)
+ end
end
end
- click_element(:labels_edit_button)
-
- labels.each do |label|
- has_element?(:labels_block, text: label, wait: 0)
- end
-
- refresh
-
- wait_for_requests
+ click_element(:title) # to blur dropdown
end
def toggle_more_assignees_link
click_element(:more_assignees_link)
end
+ # When the labels_widget feature flag is enabled, wait until the labels widget appears
+ def wait_for_labels_widget_feature_flag
+ Support::Retrier.retry_until(max_duration: 60, reload_page: page, retry_on_exception: true, sleep_interval: 5) do
+ within_element(:labels_block) do
+ find_element(:edit_link)
+ end
+ end
+ end
+
private
def wait_assignees_block_finish_loading
@@ -141,6 +144,15 @@ module QA
end
end
+ def wait_labels_block_finish_loading
+ within_element(:labels_block) do
+ wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
+ finished_loading_block?
+ yield
+ end
+ end
+ end
+
def wait_milestone_block_finish_loading
within_element(:milestone_block) do
wait_until(reload: false, max_duration: 10, sleep_interval: 1) do
diff --git a/qa/qa/page/component/issue_board/show.rb b/qa/qa/page/component/issue_board/show.rb
index 1c1f7ab17f3..2259a65b546 100644
--- a/qa/qa/page/component/issue_board/show.rb
+++ b/qa/qa/page/component/issue_board/show.rb
@@ -24,14 +24,6 @@ module QA
element :create_new_board_button
end
- view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue' do
- element :labels_dropdown_content
- end
-
- view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue' do
- element :labels_edit_button
- end
-
view 'app/assets/javascripts/boards/components/board_content.vue' do
element :boards_list
end
@@ -85,6 +77,7 @@ module QA
def click_boards_config_button
click_element(:boards_config_button)
+ wait_for_requests
end
def click_boards_dropdown_button
@@ -97,16 +90,6 @@ module QA
click_element(:focus_mode_button)
end
- def configure_by_label(label)
- click_boards_config_button
- click_element(:labels_edit_button)
- find_element(:labels_dropdown_content).find('li', text: label).click
- # Clicking the edit button again closes the dropdown and allows the save button to be clicked
- click_element(:labels_edit_button)
- click_element(:save_changes_button)
- wait_boards_list_finish_loading
- end
-
def create_new_board(board_name)
click_boards_dropdown_button
click_element(:create_new_board_button)
diff --git a/qa/qa/page/group/bulk_import.rb b/qa/qa/page/group/bulk_import.rb
index a62823f3469..90bc7a66dcc 100644
--- a/qa/qa/page/group/bulk_import.rb
+++ b/qa/qa/page/group/bulk_import.rb
@@ -34,7 +34,7 @@ module QA
click_element(:target_namespace_selector_dropdown)
click_element(:target_group_dropdown_item, group_name: target_group_name)
- retry_until do
+ retry_until(message: "Triggering import") do
click_element(:import_group_button)
# Make sure import started before waiting for completion
has_no_element?(:import_status_indicator, text: "Not started", wait: 1)
diff --git a/qa/qa/page/group/settings/package_registries.rb b/qa/qa/page/group/settings/package_registries.rb
index 5c93c0d6222..433872a378a 100644
--- a/qa/qa/page/group/settings/package_registries.rb
+++ b/qa/qa/page/group/settings/package_registries.rb
@@ -1,5 +1,4 @@
# frozen_string_literal: true
-
module QA
module Page
module Group
@@ -20,22 +19,33 @@ module QA
def set_allow_duplicates_disabled
expand_content :package_registry_settings_content do
- click_element(:allow_duplicates_toggle) if duplicates_enabled?
+ click_on_allow_duplicates_button if duplicates_enabled?
end
end
def set_allow_duplicates_enabled
expand_content :package_registry_settings_content do
- click_element(:allow_duplicates_toggle) if duplicates_disabled?
+ click_on_allow_duplicates_button unless duplicates_enabled?
+ end
+ end
+
+ def click_on_allow_duplicates_button
+ with_allow_duplicates_button do |button|
+ button.click
end
end
def duplicates_enabled?
- has_element?(:allow_duplicates_label, text: 'Allow duplicates')
+ with_allow_duplicates_button do |button|
+ button[:class].include?('is-checked')
+ end
end
- def duplicates_disabled?
- has_element?(:allow_duplicates_label, text: 'Do not allow duplicates')
+ def with_allow_duplicates_button
+ within_element :allow_duplicates_toggle do
+ toggle = find('button.gl-toggle:not(.is-disabled)')
+ yield(toggle)
+ end
end
def has_dependency_proxy_enabled?
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 38d919be4db..2cd78f9f17a 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -9,6 +9,7 @@ module QA
view 'app/views/groups/_home_panel.html.haml' do
element :new_project_button
element :new_subgroup_button
+ element :group_id_content
end
view 'app/assets/javascripts/groups/constants.js' do
@@ -40,6 +41,10 @@ module QA
click_element :new_project_button
end
+ def group_id
+ find_element(:group_id_content).text.delete('Group ID: ')
+ end
+
def leave_group
accept_alert do
click_element :leave_group_link
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index c3170478733..5cba9d4bce4 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -45,6 +45,10 @@ module QA
has_element?(:sign_in_button)
end
+ def on_login_page?
+ has_element?(:login_page, wait: 0)
+ end
+
def sign_in_using_credentials(user: nil, skip_page_validation: false)
# Don't try to log-in if we're already logged-in
return if Page::Main::Menu.perform(&:signed_in?)
@@ -164,6 +168,8 @@ module QA
fill_element :password_field, user.password
click_element :sign_in_button
+ Support::WaitForRequests.wait_for_requests
+
Page::Main::Terms.perform do |terms|
terms.accept_terms if terms.visible?
end
diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb
index ad5cd971afc..e3bb585955b 100644
--- a/qa/qa/page/main/menu.rb
+++ b/qa/qa/page/main/menu.rb
@@ -4,6 +4,8 @@ module QA
module Page
module Main
class Menu < Page::Base
+ prepend Mobile::Page::Main::Menu if Runtime::Env.mobile_layout?
+
view 'app/views/layouts/header/_current_user_dropdown.html.haml' do
element :sign_out_link
element :edit_profile_link
@@ -12,12 +14,12 @@ module QA
view 'app/views/layouts/header/_default.html.haml' do
element :navbar, required: true
element :canary_badge_link
- element :user_avatar, required: true
- element :user_menu, required: true
+ element :user_avatar, required: !QA::Runtime::Env.mobile_layout?
+ element :user_menu, required: !QA::Runtime::Env.mobile_layout?
element :stop_impersonation_link
- element :issues_shortcut_button, required: true
- element :merge_requests_shortcut_button, required: true
- element :todos_shortcut_button, required: true
+ element :issues_shortcut_button, required: !QA::Runtime::Env.mobile_layout?
+ element :merge_requests_shortcut_button, required: !QA::Runtime::Env.mobile_layout?
+ element :todos_shortcut_button, required: !QA::Runtime::Env.mobile_layout?
end
view 'app/assets/javascripts/nav/components/top_nav_app.vue' do
@@ -98,10 +100,14 @@ module QA
end
def signed_in?
+ return false if Page::Main::Login.perform(&:on_login_page?)
+
has_personal_area?(wait: 0)
end
def not_signed_in?
+ return true if Page::Main::Login.perform(&:on_login_page?)
+
has_no_personal_area?
end
@@ -115,7 +121,7 @@ module QA
click_element :sign_out_link
end
- has_no_element?(:user_avatar)
+ not_signed_in?
end
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index d4fa3b38f02..f8d063ac6bd 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -99,6 +99,8 @@ module QA
view 'app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue' do
element :add_suggestion_batch_button
+ element :applied_badge
+ element :applying_badge
end
view 'app/views/projects/merge_requests/_description.html.haml' do
@@ -354,7 +356,7 @@ module QA
end
def apply_suggestion_with_message(message)
- click_element(:apply_suggestion_dropdown)
+ all_elements(:apply_suggestion_dropdown, minimum: 1).first.click
fill_element(:commit_message_field, message)
click_element(:commit_with_custom_message_button)
end
@@ -363,6 +365,13 @@ module QA
all_elements(:add_suggestion_batch_button, minimum: 1).first.click
end
+ def has_suggestions_applied?(count = 1)
+ wait_until(reload: false) do
+ has_no_element?(:applying_badge)
+ end
+ all_elements(:applied_badge, count: count)
+ end
+
def cherry_pick!
click_element(:cherry_pick_button, Page::Component::CommitModal)
click_element(:submit_commit_button)
diff --git a/qa/qa/page/profile/menu.rb b/qa/qa/page/profile/menu.rb
index a12db2918dc..d638a378610 100644
--- a/qa/qa/page/profile/menu.rb
+++ b/qa/qa/page/profile/menu.rb
@@ -4,6 +4,10 @@ module QA
module Page
module Profile
class Menu < Page::Base
+ # We need to check remote_mobile_device_name instead of mobile_layout? here
+ # since tablets have the regular top navigation bar but still close the left nav
+ prepend QA::Mobile::Page::Profile::Menu if QA::Runtime::Env.remote_mobile_device_name
+
view 'app/views/layouts/nav/sidebar/_profile.html.haml' do
element :access_token_link, 'link_to profile_personal_access_tokens_path' # rubocop:disable QA/ElementWithPattern
element :access_token_title, 'Access Tokens' # rubocop:disable QA/ElementWithPattern
diff --git a/qa/qa/page/project/fork/new.rb b/qa/qa/page/project/fork/new.rb
index 7062702679a..cd743b648d8 100644
--- a/qa/qa/page/project/fork/new.rb
+++ b/qa/qa/page/project/fork/new.rb
@@ -12,6 +12,7 @@ module QA
view 'app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue' do
element :fork_namespace_dropdown
element :fork_project_button
+ element :fork_privacy_button
end
def fork_project(namespace = Runtime::Namespace.path)
@@ -19,6 +20,7 @@ module QA
click_element(:fork_namespace_button, name: namespace)
else
select_element(:fork_namespace_dropdown, namespace)
+ click_element(:fork_privacy_button, privacy_level: 'public')
click_element(:fork_project_button)
end
end
diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb
index bb35c5eb17c..47f7e701ae8 100644
--- a/qa/qa/page/project/import/github.rb
+++ b/qa/qa/page/project/import/github.rb
@@ -49,7 +49,12 @@ module QA
click_element(:target_namespace_selector_dropdown)
click_element(:target_group_dropdown_item, group_name: target_group_path)
fill_element(:project_path_field, project_name)
- click_element(:import_button)
+
+ retry_until do
+ click_element(:import_button)
+ # Make sure import started before waiting for completion
+ has_no_element?(:import_status_indicator, text: "Not started", wait: 1)
+ end
end
end
diff --git a/qa/qa/page/project/infrastructure/kubernetes/add.rb b/qa/qa/page/project/infrastructure/kubernetes/add.rb
index e2d50c1bcf1..ed9ecb51a46 100644
--- a/qa/qa/page/project/infrastructure/kubernetes/add.rb
+++ b/qa/qa/page/project/infrastructure/kubernetes/add.rb
@@ -11,7 +11,7 @@ module QA
end
def add_existing_cluster
- click_element(:add_existing_cluster_tab)
+ page.find('.gl-tab-nav-item', text: 'Connect existing cluster').click
end
end
end
diff --git a/qa/qa/page/project/infrastructure/kubernetes/index.rb b/qa/qa/page/project/infrastructure/kubernetes/index.rb
index bdcaf7ffaff..0424682179e 100644
--- a/qa/qa/page/project/infrastructure/kubernetes/index.rb
+++ b/qa/qa/page/project/infrastructure/kubernetes/index.rb
@@ -6,12 +6,12 @@ module QA
module Infrastructure
module Kubernetes
class Index < Page::Base
- view 'app/views/clusters/clusters/_empty_state.html.haml' do
- element :add_kubernetes_cluster_link
+ view 'app/assets/javascripts/clusters_list/components/clusters_view_all.vue' do
+ element :connect_existing_cluster_button
end
- def add_kubernetes_cluster
- click_element :add_kubernetes_cluster_link
+ def connect_existing_cluster
+ click_link 'Connect existing cluster'
end
def has_cluster?(cluster)
diff --git a/qa/qa/page/project/infrastructure/kubernetes/show.rb b/qa/qa/page/project/infrastructure/kubernetes/show.rb
index 62a04a53a2f..6de5024e525 100644
--- a/qa/qa/page/project/infrastructure/kubernetes/show.rb
+++ b/qa/qa/page/project/infrastructure/kubernetes/show.rb
@@ -7,26 +7,9 @@ module QA
module Kubernetes
class Show < Page::Base
view 'app/assets/javascripts/clusters/forms/components/integration_form.vue' do
- element :integration_status_toggle, required: true
- element :base_domain_field, required: true
- element :save_changes_button, required: true
- end
-
- view 'app/views/clusters/clusters/_details_tab.html.haml' do
- element :details, required: true
- end
-
- view 'app/views/clusters/clusters/_health.html.haml' do
- element :cluster_health_section
- end
-
- view 'app/views/clusters/clusters/_health_tab.html.haml' do
- element :health, required: true
- end
-
- def open_details
- has_element?(:details, wait: 30)
- click_element :details
+ element :integration_status_toggle
+ element :base_domain_field
+ element :save_changes_button
end
def set_domain(domain)
@@ -36,29 +19,6 @@ module QA
def save_domain
click_element :save_changes_button, Page::Project::Infrastructure::Kubernetes::Show
end
-
- def wait_for_cluster_health
- wait_until(max_duration: 120, sleep_interval: 3, reload: true) do
- has_cluster_health_graphs?
- end
- end
-
- def open_health
- has_element?(:health, wait: 30)
- click_element :health
- end
-
- def has_cluster_health_graphs?
- within_cluster_health_section do
- has_text?('CPU Usage')
- end
- end
-
- def within_cluster_health_section
- within_element :cluster_health_section do
- yield
- end
- end
end
end
end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index 7a5a153db86..3b033830420 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -9,6 +9,7 @@ module QA
include Page::Component::Note
include Page::Component::DesignManagement
include Page::Component::Issuable::Sidebar
+ prepend Mobile::Page::Project::Issue::Show if Runtime::Env.mobile_layout?
view 'app/assets/javascripts/vue_shared/components/issue/related_issuable_item.vue' do
element :remove_related_issue_button
@@ -64,6 +65,10 @@ module QA
def has_metrics_unfurled?
has_element?(:prometheus_graph_widgets, wait: 30)
end
+
+ def has_reopen_issue_button?
+ has_element?(:reopen_issue_button)
+ end
end
end
end
diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb
index 78b6bebe02e..2fb925b3930 100644
--- a/qa/qa/page/project/job/show.rb
+++ b/qa/qa/page/project/job/show.rb
@@ -62,6 +62,12 @@ module QA
has_element? :job_log_content
end
+ def has_status?(status, wait: 30)
+ wait_until(reload: false, max_duration: wait, sleep_interval: 1) do
+ status_badge == status
+ end
+ end
+
private
def loaded?(wait: 60)
diff --git a/qa/qa/page/project/monitor/metrics/show.rb b/qa/qa/page/project/monitor/metrics/show.rb
index 0129ee06cb6..70ebc795595 100644
--- a/qa/qa/page/project/monitor/metrics/show.rb
+++ b/qa/qa/page/project/monitor/metrics/show.rb
@@ -31,7 +31,6 @@ module QA
view 'app/assets/javascripts/monitoring/components/dashboard_panel.vue' do
element :prometheus_graph_widgets
element :prometheus_widgets_dropdown
- element :alert_widget_menu_item
element :generate_chart_link_menu_item
end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index 3ecdabeeed2..5ff52527774 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -13,6 +13,7 @@ module QA
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
+ element :initialize_with_sast_checkbox
element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern
element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
@@ -79,6 +80,13 @@ module QA
choose visibility.capitalize
end
+ # Disable experiment for SAST at project creation https://gitlab.com/gitlab-org/gitlab/-/issues/333196
+ def disable_initialize_with_sast
+ return unless has_element?(:initialize_with_sast_checkbox)
+
+ uncheck_element(:initialize_with_sast_checkbox)
+ end
+
def click_github_link
click_link 'GitHub'
end
diff --git a/qa/qa/page/project/registry/show.rb b/qa/qa/page/project/registry/show.rb
index f2472a83401..270445560be 100644
--- a/qa/qa/page/project/registry/show.rb
+++ b/qa/qa/page/project/registry/show.rb
@@ -5,15 +5,15 @@ module QA
module Project
module Registry
class Show < QA::Page::Base
- view 'app/assets/javascripts/registry/explorer/components/list_page/image_list_row.vue' do
+ view 'app/assets/javascripts/packages_and_registries/container_registry/explorer/components/list_page/image_list_row.vue' do
element :registry_image_content
end
- view 'app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue' do
+ view 'app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue' do
element :more_actions_menu
end
- view 'app/assets/javascripts/registry/explorer/components/details_page/tags_list_row.vue' do
+ view 'app/assets/javascripts/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row.vue' do
element :tag_delete_button
end
diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb
index 7b61c81154a..407d57bc54e 100644
--- a/qa/qa/page/project/settings/deploy_tokens.rb
+++ b/qa/qa/page/project/settings/deploy_tokens.rb
@@ -12,6 +12,7 @@ module QA
element :deploy_token_read_package_registry_checkbox
element :deploy_token_write_package_registry_checkbox
element :deploy_token_read_registry_checkbox
+ element :deploy_token_write_registry_checkbox
element :create_deploy_token_button
end
@@ -29,11 +30,12 @@ module QA
fill_element(:deploy_token_expires_at_field, expires_at.to_s + "\n")
end
- def fill_scopes(read_repository: false, read_registry: false, read_package_registry: false, write_package_registry: false)
- check_element(:deploy_token_read_repository_checkbox) if read_repository
- check_element(:deploy_token_read_package_registry_checkbox) if read_package_registry
- check_element(:deploy_token_write_package_registry_checkbox) if write_package_registry
- check_element(:deploy_token_read_registry_checkbox) if read_registry
+ def fill_scopes(scopes)
+ check_element(:deploy_token_read_repository_checkbox) if scopes.include? :read_repository
+ check_element(:deploy_token_read_package_registry_checkbox) if scopes.include? :read_package_registry
+ check_element(:deploy_token_write_package_registry_checkbox) if scopes.include? :write_package_registry
+ check_element(:deploy_token_read_registry_checkbox) if scopes.include? :read_registry
+ check_element(:deploy_token_write_registry_checkbox) if scopes.include? :write_registry
end
def add_token
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 6e5097c3812..65a1f726a8a 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -9,6 +9,7 @@ module QA
include Page::Component::Breadcrumbs
include Page::Project::SubMenus::Settings
include Page::File::Shared::CommitMessage
+ prepend Mobile::Page::Project::Show if Runtime::Env.mobile_layout?
view 'app/assets/javascripts/repository/components/preview/index.vue' do
element :blob_viewer_content
@@ -117,7 +118,7 @@ module QA
end
def go_to_new_issue
- click_element :new_menu_toggle
+ click_element(:new_menu_toggle)
click_element(:new_issue_link)
end
@@ -153,6 +154,10 @@ module QA
click_element(:web_ide_button)
end
+ def open_web_ide_via_shortcut
+ page.driver.send_keys('.')
+ end
+
def has_edit_fork_button?
has_element?(:web_ide_button, text: 'Edit fork in Web IDE')
end
diff --git a/qa/qa/page/project/sub_menus/common.rb b/qa/qa/page/project/sub_menus/common.rb
index c20710bc393..112f49a90ee 100644
--- a/qa/qa/page/project/sub_menus/common.rb
+++ b/qa/qa/page/project/sub_menus/common.rb
@@ -19,6 +19,10 @@ module QA
view 'app/views/shared/nav/_sidebar_menu.html.haml' do
element :sidebar_menu_link
end
+
+ view 'app/views/layouts/nav/_breadcrumbs.html.haml' do
+ element :toggle_mobile_nav_button
+ end
end
end
diff --git a/qa/qa/page/sub_menus/common.rb b/qa/qa/page/sub_menus/common.rb
index 2efeeb062e8..518b3b4e84e 100644
--- a/qa/qa/page/sub_menus/common.rb
+++ b/qa/qa/page/sub_menus/common.rb
@@ -4,6 +4,10 @@ module QA
module Page
module SubMenus
module Common
+ # We need to check remote_mobile_device_name instead of mobile_layout? here
+ # since tablets have the regular top navigation bar but still close the left nav
+ prepend Mobile::Page::SubMenus::Common if QA::Runtime::Env.remote_mobile_device_name
+
def hover_element(element)
within_sidebar do
find_element(element).hover
diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb
index a7243b7ebc2..26a2a668cc1 100644
--- a/qa/qa/resource/base.rb
+++ b/qa/qa/resource/base.rb
@@ -77,17 +77,24 @@ module QA
def log_fabrication(method, resource, parents, args)
start = Time.now
- yield.tap do
+ Support::FabricationTracker.start_fabrication
+ result = yield.tap do
+ fabrication_time = Time.now - start
+
+ Support::FabricationTracker.save_fabrication(:"#{method}_fabrication", fabrication_time)
Runtime::Logger.debug do
msg = ["==#{'=' * parents.size}>"]
msg << "Built a #{name}"
msg << "as a dependency of #{parents.last}" if parents.any?
msg << "via #{method}"
- msg << "in #{Time.now - start} seconds"
+ msg << "in #{fabrication_time} seconds"
msg.join(' ')
end
end
+ Support::FabricationTracker.finish_fabrication
+
+ result
end
# Define custom attribute
diff --git a/qa/qa/resource/bulk_import_group.rb b/qa/qa/resource/bulk_import_group.rb
index 5380bb16f10..e8dc2d291b8 100644
--- a/qa/qa/resource/bulk_import_group.rb
+++ b/qa/qa/resource/bulk_import_group.rb
@@ -59,6 +59,9 @@ module QA
}
end
+ # Get import status
+ #
+ # @return [String]
def import_status
response = get(Runtime::API::Request.new(api_client, "/bulk_imports/#{import_id}").url)
@@ -69,6 +72,15 @@ module QA
parse_body(response)[:status]
end
+ # Get import details
+ #
+ # @return [Array]
+ def import_details
+ response = get(Runtime::API::Request.new(api_client, "/bulk_imports/#{import_id}/entities").url)
+
+ parse_body(response)
+ end
+
private
def transform_api_resource(api_resource)
diff --git a/qa/qa/resource/clusters/agent.rb b/qa/qa/resource/clusters/agent.rb
index cad5a4c6b1d..ee5a292b9b3 100644
--- a/qa/qa/resource/clusters/agent.rb
+++ b/qa/qa/resource/clusters/agent.rb
@@ -19,13 +19,12 @@ module QA
def fabricate!
puts 'TODO: FABRICATE VIA UI'
end
- # TODO
- #
- # The UI for this model is not yet implemented. So far it can only be
- # created through the GraphQL API
- # def fabricate
- #
- # end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+ end
def api_get_path
"gid://gitlab/Clusters::Agent/#{id}"
diff --git a/qa/qa/resource/clusters/agent_token.rb b/qa/qa/resource/clusters/agent_token.rb
index 3286f46cdb2..6d803b94564 100644
--- a/qa/qa/resource/clusters/agent_token.rb
+++ b/qa/qa/resource/clusters/agent_token.rb
@@ -13,13 +13,12 @@ module QA
def fabricate!
puts 'TODO: FABRICATE VIA UI'
end
- # TODO
- #
- # The UI for this model is not yet implemented. So far it can only be
- # created through the GraphQL API
- # def fabricate
- #
- # end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+ end
def api_get_path
"gid://gitlab/Clusters::AgentToken/#{id}"
diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb
index 151454c37b1..f5d3b87fc2b 100644
--- a/qa/qa/resource/deploy_token.rb
+++ b/qa/qa/resource/deploy_token.rb
@@ -4,6 +4,7 @@ module QA
module Resource
class DeployToken < Base
attr_accessor :name, :expires_at
+ attr_writer :scopes
attribute :username do
Page::Project::Settings::Repository.perform do |repository_page|
@@ -37,7 +38,7 @@ module QA
setting.expand_deploy_tokens do |page|
page.fill_token_name(name)
page.fill_token_expires_at(expires_at)
- page.fill_scopes(read_repository: true, read_package_registry: true, write_package_registry: true)
+ page.fill_scopes(@scopes)
page.add_token
end
diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb
index 4ca180373f6..9b05c0cb456 100644
--- a/qa/qa/resource/file.rb
+++ b/qa/qa/resource/file.rb
@@ -67,7 +67,7 @@ module QA
private
def transform_api_resource(api_resource)
- api_resource[:web_url] = "#{Runtime::Scenario.gitlab_address}/#{project.full_path}/-/tree/#{branch}/#{api_resource[:file_path]}"
+ api_resource[:web_url] = "#{Runtime::Scenario.gitlab_address}/#{project.full_path}/-/blob/#{branch}/#{api_resource[:file_path]}"
api_resource
end
end
diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb
index b3814011f2c..d0313670e8b 100644
--- a/qa/qa/resource/fork.rb
+++ b/qa/qa/resource/fork.rb
@@ -37,11 +37,9 @@ module QA
namespace_path ||= user.name
# Sign out as admin and sign is as the fork user
- Page::Main::Menu.perform(&:sign_out)
- Runtime::Browser.visit(:gitlab, Page::Main::Login)
- Page::Main::Login.perform do |login|
- login.sign_in_using_credentials(user: user)
- end
+ Flow::Login.sign_in(as: user)
+
+ @api_client = Runtime::API::Client.new(:gitlab, is_new_session: false, user: user)
upstream.visit!
diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb
index 9214d4eff4a..344f177932f 100644
--- a/qa/qa/resource/issue.rb
+++ b/qa/qa/resource/issue.rb
@@ -9,6 +9,7 @@ module QA
Project.fabricate! do |resource|
resource.name = 'project-for-issues'
resource.description = 'project for adding issues'
+ resource.api_client = api_client
end
end
@@ -93,6 +94,52 @@ module QA
attempts: attempts
)
end
+
+ # Object comparison
+ #
+ # @param [QA::Resource::Issue] other
+ # @return [Boolean]
+ def ==(other)
+ other.is_a?(Issue) && comparable_issue == other.comparable_issue
+ end
+
+ # Override inspect for a better rspec failure diff output
+ #
+ # @return [String]
+ def inspect
+ JSON.pretty_generate(comparable_issue)
+ end
+
+ protected
+
+ # Return subset of fields for comparing issues
+ #
+ # @return [Hash]
+ def comparable_issue
+ reload! if api_response.nil?
+
+ api_resource.slice(
+ :state,
+ :description,
+ :type,
+ :title,
+ :labels,
+ :milestone,
+ :upvotes,
+ :downvotes,
+ :merge_requests_count,
+ :user_notes_count,
+ :due_date,
+ :has_tasks,
+ :task_status,
+ :confidential,
+ :discussion_locked,
+ :issue_type,
+ :task_completion_status,
+ :closed_at,
+ :created_at
+ )
+ end
end
end
end
diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
index b3eba77fc46..0a63ff47694 100644
--- a/qa/qa/resource/kubernetes_cluster/project_cluster.rb
+++ b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
@@ -13,8 +13,8 @@ module QA
Resource::Project.fabricate!
end
- def ingress_ip
- @ingress_ip ||= @cluster.fetch_external_ip_for_ingress
+ attribute :ingress_ip do
+ @cluster.fetch_external_ip_for_ingress
end
def fabricate!
@@ -24,7 +24,7 @@ module QA
&:go_to_infrastructure_kubernetes)
Page::Project::Infrastructure::Kubernetes::Index.perform(
- &:add_kubernetes_cluster)
+ &:connect_existing_cluster)
Page::Project::Infrastructure::Kubernetes::Add.perform(
&:add_existing_cluster)
@@ -39,14 +39,10 @@ module QA
end
Page::Project::Infrastructure::Kubernetes::Show.perform do |show|
- # We must wait a few seconds for permissions to be set up correctly for new cluster
- sleep 25
-
if @install_ingress
- populate(:ingress_ip)
+ ingress_ip
- show.open_details
- show.set_domain("#{ingress_ip}.nip.io")
+ show.set_domain("#{@ingress_ip}.nip.io")
show.save_domain
end
end
diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb
index b0579cf37b8..512f3eb7bfc 100644
--- a/qa/qa/resource/merge_request_from_fork.rb
+++ b/qa/qa/resource/merge_request_from_fork.rb
@@ -6,7 +6,7 @@ module QA
attr_accessor :fork_branch
attribute :fork do
- Fork.fabricate_via_api!
+ Fork.fabricate_via_browser_ui!
end
attribute :push do
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index 3f6a4eee5ac..864f3a14c3d 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -27,7 +27,9 @@ module QA
:import_error
attribute :group do
- Group.fabricate!
+ Group.fabricate! do |group|
+ group.api_client = api_client
+ end
end
attribute :path_with_namespace do
@@ -91,6 +93,7 @@ module QA
new_page.choose_name(@name)
new_page.add_description(@description)
new_page.set_visibility(@visibility)
+ new_page.disable_initialize_with_sast
new_page.disable_initialize_with_readme unless @initialize_with_readme
new_page.create_new_project
end
@@ -214,6 +217,10 @@ module QA
"#{api_get_path}/wikis"
end
+ def api_push_rules_path
+ "#{api_get_path}/push_rule"
+ end
+
def api_post_body
post_body = {
name: name,
@@ -358,6 +365,15 @@ module QA
parse_body(response)
end
+ def push_rules
+ response = get(request_url(api_push_rules_path))
+ parse_body(response)
+ end
+
+ def add_push_rules(rules)
+ api_post_to(api_push_rules_path, rules)
+ end
+
# Object comparison
#
# @param [QA::Resource::Project] other
diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb
index b351d78a184..385e0fa4f7e 100644
--- a/qa/qa/resource/sandbox.rb
+++ b/qa/qa/resource/sandbox.rb
@@ -7,6 +7,17 @@ module QA
# creating it if it doesn't yet exist.
#
class Sandbox < GroupBase
+ class << self
+ # Force top level group creation via UI if test is executed on dot_com environment
+ def fabricate!(*args, &prepare_block)
+ return fabricate_via_browser_ui!(*args, &prepare_block) if Specs::Helpers::ContextSelector.dot_com?
+
+ fabricate_via_api!(*args, &prepare_block)
+ rescue NotImplementedError
+ fabricate_via_browser_ui!(*args, &prepare_block)
+ end
+ end
+
def initialize
@path = Runtime::Namespace.sandbox_name
end
@@ -14,6 +25,8 @@ module QA
alias_method :full_path, :path
def fabricate!
+ Flow::Login.sign_in_unless_signed_in
+
Page::Main::Menu.perform(&:go_to_groups)
Page::Dashboard::Groups.perform do |groups_page|
@@ -23,10 +36,13 @@ module QA
groups_page.click_new_group
Page::Group::New.perform do |group|
+ group.click_create_group
group.set_path(path)
group.set_visibility('Public')
group.create
end
+
+ @id = Page::Group::Show.perform(&:group_id)
end
end
end
diff --git a/qa/qa/runtime/allure_report.rb b/qa/qa/runtime/allure_report.rb
index 5f628050f3b..9ae04dbe111 100644
--- a/qa/qa/runtime/allure_report.rb
+++ b/qa/qa/runtime/allure_report.rb
@@ -76,6 +76,15 @@ module QA
RSpec.configure do |config|
config.add_formatter(AllureRspecFormatter)
config.add_formatter(QA::Support::Formatters::AllureMetadataFormatter)
+
+ config.append_after do |example|
+ Allure.add_attachment(
+ name: 'browser.log',
+ source: Capybara.current_session.driver.browser.logs.get(:browser).map(&:to_s).join("\n\n"),
+ type: Allure::ContentType::TXT,
+ test_case: true
+ )
+ end
end
end
diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb
index 0566bc237bb..f1d93ce376a 100644
--- a/qa/qa/runtime/browser.rb
+++ b/qa/qa/runtime/browser.rb
@@ -99,7 +99,7 @@ module QA
end
# Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab/issues/4252
- capabilities['goog:chromeOptions'][:args] << 'disable-dev-shm-usage' if QA::Runtime::Env.running_in_ci?
+ capabilities['goog:chromeOptions'][:args] << 'disable-dev-shm-usage' if QA::Runtime::Env.disable_dev_shm?
# Specify the user-agent to allow challenges to be bypassed
# See https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/11938
@@ -205,6 +205,9 @@ module QA
simulate_slow_connection if Runtime::Env.simulate_slow_connection?
+ # Wait until the new page is ready for us to interact with it
+ Support::WaitForRequests.wait_for_requests
+
page_class.validate_elements_present! if page_class.respond_to?(:validate_elements_present!)
if QA::Runtime::Env.qa_cookies
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index cdfa95457c7..163710a1510 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -80,6 +80,11 @@ module QA
enabled?(ENV['CHROME_REUSE_PROFILE'], default: false)
end
+ # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab/issues/4252
+ def disable_dev_shm?
+ running_in_ci? || enabled?(ENV['CHROME_DISABLE_DEV_SHM'], default: false)
+ end
+
def accept_insecure_certs?
enabled?(ENV['ACCEPT_INSECURE_CERTS'])
end
@@ -153,6 +158,12 @@ module QA
ENV['QA_REMOTE_MOBILE_DEVICE_NAME']
end
+ def mobile_layout?
+ return false if ENV['QA_REMOTE_MOBILE_DEVICE_NAME'].blank?
+
+ !(ENV['QA_REMOTE_MOBILE_DEVICE_NAME'].downcase.include?('ipad') || ENV['QA_REMOTE_MOBILE_DEVICE_NAME'].downcase.include?('tablet'))
+ end
+
def user_username
ENV['GITLAB_USERNAME']
end
@@ -392,7 +403,7 @@ module QA
end
def gitlab_agentk_version
- ENV.fetch('GITLAB_AGENTK_VERSION', 'v13.7.0')
+ ENV.fetch('GITLAB_AGENTK_VERSION', 'v14.4.0')
end
def transient_trials
diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb
index 58408524f54..ec28813c1f6 100644
--- a/qa/qa/runtime/feature.rb
+++ b/qa/qa/runtime/feature.rb
@@ -5,15 +5,16 @@ require 'active_support/core_ext/object/blank'
module QA
module Runtime
class Feature
+ SetFeatureError = Class.new(RuntimeError)
+ AuthorizationError = Class.new(RuntimeError)
+ UnknownScopeError = Class.new(RuntimeError)
+ UnknownStateError = Class.new(RuntimeError)
+
class << self
# Documentation: https://docs.gitlab.com/ee/api/features.html
include Support::API
- SetFeatureError = Class.new(RuntimeError)
- AuthorizationError = Class.new(RuntimeError)
- UnknownScopeError = Class.new(RuntimeError)
-
def remove(key)
request = Runtime::API::Request.new(api_client, "/features/#{key}")
response = delete(request.url)
@@ -30,6 +31,23 @@ module QA
set_and_verify(key, enable: false, **scopes)
end
+ # Set one or more flags to their specified state.
+ #
+ # @param [Hash] flags The feature flags and desired values, e.g., { 'flag1' => 'enabled', 'flag2' => "disabled" }
+ # @param [Hash] scopes The scope (user, project, group) to apply the feature flag to.
+ def set(flags, **scopes)
+ flags.each_pair do |flag, state|
+ case state
+ when 'enabled', 'enable', 'true', 1, true
+ enable(flag, **scopes)
+ when 'disabled', 'disable', 'false', 0, false
+ disable(flag, **scopes)
+ else
+ raise UnknownStateError, "Unknown feature flag state: #{state}"
+ end
+ end
+ end
+
def enabled?(key, **scopes)
feature = JSON.parse(get_features).find { |flag| flag['name'] == key.to_s }
feature && (feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], **scopes))
@@ -47,15 +65,15 @@ module QA
scopes.each do |key, value|
case key
when :project, :group, :user
- actors = gates.filter { |i| i['key'] == 'actors' }.first['value']
- break actors.include?("#{key.to_s.capitalize}:#{value.id}")
+ actors = gates.find { |i| i['key'] == 'actors' }['value']
+ return actors.include?("#{key.to_s.capitalize}:#{value.id}")
when :feature_group
- groups = gates.filter { |i| i['key'] == 'groups' }.first['value']
- break groups.include?(value)
- else
- raise UnknownScopeError, "Unknown scope: #{key}"
+ groups = gates.find { |i| i['key'] == 'groups' }['value']
+ return groups.include?(value)
end
end
+
+ raise UnknownScopeError, "Unknown scope in: #{scopes}"
end
def get_features
diff --git a/qa/qa/scenario/bootable.rb b/qa/qa/scenario/bootable.rb
index 841ac4dd560..ae180ffce1c 100644
--- a/qa/qa/scenario/bootable.rb
+++ b/qa/qa/scenario/bootable.rb
@@ -17,6 +17,22 @@ module QA
arguments = OptionParser.new do |parser|
options.to_a.each do |opt|
+ # The argument for the --set-feature-flags option should look something like "flag1=enabled,flag2=disabled"
+ # Here we translate that string into a hash, e.g.: { 'flag1' => 'enabled', 'flag2' => "disabled" }
+ if opt.name == :set_feature_flags
+ parser.on(opt.arg, opt.desc) do |flags|
+ value = flags.split(',').each_with_object({}) do |pair, hash|
+ flag_name, flag_value = pair.split('=')
+ raise '--set-feature-flags requires flag name and flag state for each flag, e.g., flag1=enabled,flag2=disabled' unless flag_name && flag_value
+
+ hash[flag_name] = flag_value
+ end
+ Runtime::Scenario.define(opt.name, value)
+ end
+
+ next
+ end
+
parser.on(opt.arg, opt.desc) do |value|
Runtime::Scenario.define(opt.name, value)
end
diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb
index e2eaca42277..ddbe28f05d9 100644
--- a/qa/qa/scenario/shared_attributes.rb
+++ b/qa/qa/scenario/shared_attributes.rb
@@ -8,6 +8,9 @@ module QA
attribute :gitlab_address, '--address URL', 'Address of the instance to test'
attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests'
attribute :disable_feature, '--disable-feature FEATURE_FLAG', 'Disable a feature before running tests'
+ attribute :set_feature_flags, '--set-feature-flags FEATURE_FLAGS',
+ 'Set one or more feature flags before running tests. ' \
+ 'Specify FEATURE_FLAGS as comma-separated flag=state pairs, e.g., "flag1=enabled,flag2=disabled"'
attribute :parallel, '--parallel', 'Execute tests in parallel'
attribute :loop, '--loop', 'Execute test repeatedly'
end
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
index d0a201e3d22..50bb952f1fd 100644
--- a/qa/qa/scenario/template.rb
+++ b/qa/qa/scenario/template.rb
@@ -38,8 +38,8 @@ module QA
Runtime::Release.perform_before_hooks
Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature)
-
Runtime::Feature.disable(options[:disable_feature]) if options.key?(:disable_feature) && (@feature_enabled = Runtime::Feature.enabled?(options[:disable_feature]))
+ Runtime::Feature.set(options[:set_feature_flags]) if options.key?(:set_feature_flags)
Specs::Runner.perform do |specs|
specs.tty = true
diff --git a/qa/qa/scenario/test/instance/reliable.rb b/qa/qa/scenario/test/instance/reliable.rb
new file mode 100644
index 00000000000..725ab59f24a
--- /dev/null
+++ b/qa/qa/scenario/test/instance/reliable.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Instance
+ class Reliable < Template
+ include Bootable
+ include SharedAttributes
+
+ tags :reliable
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/ldap_no_tls.rb b/qa/qa/scenario/test/integration/ldap_no_tls.rb
index bbf4c847f33..19f62b6ed80 100644
--- a/qa/qa/scenario/test/integration/ldap_no_tls.rb
+++ b/qa/qa/scenario/test/integration/ldap_no_tls.rb
@@ -1,3 +1,4 @@
+# rubocop:todo Naming/FileName
# frozen_string_literal: true
module QA
@@ -11,3 +12,5 @@ module QA
end
end
end
+
+# rubocop:enable Naming/FileName
diff --git a/qa/qa/scenario/test/integration/ldap_tls.rb b/qa/qa/scenario/test/integration/ldap_tls.rb
index 2a767e57bc6..109fbe6fd74 100644
--- a/qa/qa/scenario/test/integration/ldap_tls.rb
+++ b/qa/qa/scenario/test/integration/ldap_tls.rb
@@ -1,3 +1,4 @@
+# rubocop:todo Naming/FileName
# frozen_string_literal: true
module QA
@@ -11,3 +12,5 @@ module QA
end
end
end
+
+# rubocop:enable Naming/FileName
diff --git a/qa/qa/scenario/test/integration/registry_tls.rb b/qa/qa/scenario/test/integration/registry_tls.rb
new file mode 100644
index 00000000000..4e9d6b6ea97
--- /dev/null
+++ b/qa/qa/scenario/test/integration/registry_tls.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class RegistryTLS < Test::Instance::All
+ tags :registry_tls
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/cluster_provider/gcloud.rb b/qa/qa/service/cluster_provider/gcloud.rb
index f0fb5eee6e3..c6d1f6cfe88 100644
--- a/qa/qa/service/cluster_provider/gcloud.rb
+++ b/qa/qa/service/cluster_provider/gcloud.rb
@@ -24,16 +24,6 @@ module QA
)
end
- def set_credentials(admin_user)
- master_auth = JSON.parse(`gcloud container clusters describe #{cluster_name} --region #{@region} --format 'json(masterAuth.username, masterAuth.password)'`)
-
- shell <<~CMD.tr("\n", ' ')
- kubectl config set-credentials #{admin_user}
- --username #{master_auth['masterAuth']['username']}
- --password #{master_auth['masterAuth']['password']}
- CMD
- end
-
def setup
login_if_not_already_logged_in
create_cluster
@@ -43,6 +33,12 @@ module QA
delete_cluster
end
+ def install_ingress
+ QA::Runtime::Logger.info "Attempting to install Ingress on cluster #{cluster_name}"
+ shell 'kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.31.0/deploy/static/provider/cloud/deploy.yaml'
+ wait_for_ingress
+ end
+
private
def login_if_not_already_logged_in
@@ -59,7 +55,7 @@ module QA
end
def attempt_login_with_env_vars
- puts "No gcloud account. Attempting to login from env vars GCLOUD_ACCOUNT_EMAIL and GCLOUD_ACCOUNT_KEY."
+ QA::Runtime::Logger.debug("Logging in with GCLOUD_ACCOUNT_EMAIL and GCLOUD_ACCOUNT_KEY.")
gcloud_account_key = Tempfile.new('gcloud-account-key')
gcloud_account_key.write(Runtime::Env.gcloud_account_key)
gcloud_account_key.close
@@ -80,7 +76,6 @@ module QA
gcloud container clusters
create #{cluster_name}
#{auth_options}
- --enable-basic-auth
--region #{@region}
--disk-size 10GB
--num-nodes #{Runtime::Env.gcloud_num_nodes}
@@ -109,6 +104,18 @@ module QA
def get_region
Runtime::Env.gcloud_region || @available_regions.delete(@available_regions.sample)
end
+
+ def wait_for_ingress
+ QA::Runtime::Logger.info 'Waiting for Ingress controller pod to be initialized'
+
+ Support::Retrier.retry_until(max_attempts: 60, sleep_interval: 1) do
+ service_available?('kubectl get pods --all-namespaces -l app.kubernetes.io/component=controller | grep -o "ingress-nginx-controller.*1/1"')
+ end
+ end
+
+ def service_available?(command)
+ system("#{command} > /dev/null 2>&1")
+ end
end
end
end
diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb
index 674bcdca9bb..ec53b9d8163 100644
--- a/qa/qa/service/kubernetes_cluster.rb
+++ b/qa/qa/service/kubernetes_cluster.rb
@@ -41,6 +41,10 @@ module QA
cluster_name
end
+ def install_ingress
+ @provider.install_ingress
+ end
+
def create_secret(secret, secret_name)
shell("kubectl create secret generic #{secret_name} --from-literal=token='#{secret}'")
end
@@ -70,7 +74,13 @@ module QA
end
def fetch_external_ip_for_ingress
- `kubectl get svc --all-namespaces --no-headers=true -l app.kubernetes.io/name=ingress-nginx -o custom-columns=:'status.loadBalancer.ingress[0].ip' | grep -v 'none'`
+ install_ingress
+
+ # need to wait since the ingress-nginx service has an initial delay set of 10 seconds
+ sleep 10
+ ingress_ip = `kubectl get svc --all-namespaces --no-headers=true -l app.kubernetes.io/name=ingress-nginx -o custom-columns=:'status.loadBalancer.ingress[0].ip' | grep -v 'none'`
+ QA::Runtime::Logger.debug "Has ingress address set to: #{ingress_ip}"
+ ingress_ip
end
private
@@ -82,7 +92,6 @@ module QA
def fetch_credentials
return global_credentials unless rbac
- @provider.set_credentials(admin_user)
create_service_account(admin_user)
account_credentials
end
diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb
index 71e3383a534..dbb49f18881 100644
--- a/qa/qa/service/praefect_manager.rb
+++ b/qa/qa/service/praefect_manager.rb
@@ -19,6 +19,8 @@ module QA
@virtual_storage = 'default'
end
+ attr_reader :primary_node, :secondary_node, :tertiary_node
+
# Executes the praefect `dataloss` command.
#
# @return [Boolean] whether dataloss has occurred
@@ -376,7 +378,6 @@ module QA
select job from replication_queue
where state = 'ready'
and job ->> 'change' = 'update'
- and job ->> 'source_node_storage' = '#{current_primary_node}'
and job ->> 'target_node_storage' = '#{@primary_node}';
SQL
) do |line|
@@ -396,6 +397,97 @@ module QA
result.size >= 5
end
+ def list_untracked_repositories
+ untracked_repositories = []
+ shell "docker exec #{@praefect} bash -c 'gitlab-ctl praefect list-untracked-repositories'" do |line|
+ # Results look like this depending on whether untracked items found or not
+ # Running list-untracked-repositories
+ # Done.
+
+ # Running list-untracked-repositories
+ # {"relative_path":"@hashed/aa/bb.git","storage":"gitaly1","virtual_storage":"default"}
+ # {"relative_path":"@hashed/bb/cc.git","storage":"gitaly3","virtual_storage":"default"}
+ # Done.
+
+ QA::Runtime::Logger.debug(line.chomp)
+ next if line.start_with?('Running list-untracked-repositories')
+ next if line.start_with?('Done.')
+
+ untracked_repositories.append(JSON.parse(line))
+ end
+
+ QA::Runtime::Logger.debug("list_untracked_repositories --- #{untracked_repositories}")
+ untracked_repositories
+ end
+
+ def track_repository_in_praefect(relative_path, storage, virtual_storage)
+ cmd = "gitlab-ctl praefect track-repository --repository-relative-path #{relative_path} --authoritative-storage #{storage} --virtual-storage-name #{virtual_storage}"
+ shell "docker exec #{@praefect} bash -c '#{cmd}'"
+ end
+
+ def remove_tracked_praefect_repository(relative_path, virtual_storage)
+ cmd = "gitlab-ctl praefect remove-repository --repository-relative-path #{relative_path} --virtual-storage-name #{virtual_storage}"
+ shell "docker exec #{@praefect} bash -c '#{cmd}'"
+ end
+
+ def add_repo_to_disk(node, repo_path)
+ cmd = "GIT_DIR=. git init --initial-branch=main /var/opt/gitlab/git-data/repositories/#{repo_path}"
+ shell "docker exec --user git #{node} bash -c '#{cmd}'"
+ end
+
+ def remove_repo_from_disk(repo_path)
+ cmd = "rm -rf /var/opt/gitlab/git-data/repositories/#{repo_path}"
+ shell "docker exec #{@primary_node} bash -c '#{cmd}'"
+ shell "docker exec #{@secondary_node} bash -c '#{cmd}'"
+ shell "docker exec #{@tertiary_node} bash -c '#{cmd}'"
+ end
+
+ def remove_repository_from_praefect_database(relative_path)
+ shell sql_to_docker_exec_cmd("delete from repositories where relative_path = '#{relative_path}';")
+ shell sql_to_docker_exec_cmd("delete from storage_repositories where relative_path = '#{relative_path}';")
+ end
+
+ def praefect_database_tracks_repo?(relative_path)
+ storage_repositories = []
+ shell sql_to_docker_exec_cmd("SELECT count(*) FROM storage_repositories where relative_path='#{relative_path}';") do |line|
+ storage_repositories << line
+ end
+ QA::Runtime::Logger.debug("storage_repositories count is ---#{storage_repositories}")
+
+ repositories = []
+ shell sql_to_docker_exec_cmd("SELECT count(*) FROM repositories where relative_path='#{relative_path}';") do |line|
+ repositories << line
+ end
+ QA::Runtime::Logger.debug("repositories count is ---#{repositories}")
+
+ (storage_repositories[2].to_i >= 1) && (repositories[2].to_i >= 1)
+ end
+
+ def repository_replicated_to_disk?(node, relative_path)
+ Support::Waiter.wait_until(max_duration: 300, sleep_interval: 3, raise_on_failure: false) do
+ result = []
+ shell sql_to_docker_exec_cmd("SELECT count(*) FROM storage_repositories where relative_path='#{relative_path}';") do |line|
+ result << line
+ end
+ QA::Runtime::Logger.debug("result is ---#{result}")
+ result[2].to_i == 3
+ end
+
+ repository_exists_on_node_disk?(node, relative_path)
+ end
+
+ def repository_exists_on_node_disk?(node, relative_path)
+ # If the dir does not exist it has a non zero exit code leading to a error being raised
+ # Instead we echo a test line if the dir does not exist, which has a zero exit code, with no output
+ bash_command = "test -d /var/opt/gitlab/git-data/repositories/#{relative_path} || echo -n 'DIR_DOES_NOT_EXIST'"
+ result = []
+ shell "docker exec #{node} bash -c '#{bash_command}'" do |line|
+ result << line
+ end
+ QA::Runtime::Logger.debug("result is ---#{result}")
+ result.exclude?("DIR_DOES_NOT_EXIST")
+ end
+
private
def current_primary_node
diff --git a/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb
index 8b4900957c5..158881ed94c 100644
--- a/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :requires_admin do
+ # run only base UI validation on staging because test requires top level group creation which is problematic
+ # on staging environment
+ RSpec.describe 'Manage', :requires_admin, except: { subdomain: :staging } do
describe 'Bulk group import' do
- let!(:staging?) { Runtime::Scenario.gitlab_address.include?('staging.gitlab.com') }
-
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
let(:admin_api_client) { Runtime::API::Client.as_admin }
let(:user) do
@@ -38,11 +38,13 @@ module QA
end
before do
- Runtime::Feature.enable(:top_level_group_creation_enabled) if staging?
-
sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
end
+ after do
+ user.remove_via_api!
+ end
+
context 'with subgroups and labels' do
let(:subgroup) do
Resource::Group.fabricate_via_api! do |group|
@@ -155,12 +157,6 @@ module QA
expect(imported_member.access_level).to eq(Resource::Members::AccessLevel::DEVELOPER)
end
end
-
- after do
- user.remove_via_api!
- ensure
- Runtime::Feature.disable(:top_level_group_creation_enabled) if staging?
- end
end
end
end
diff --git a/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb b/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb
index 9935908d55e..14c94e99446 100644
--- a/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :requires_admin do
+ # run only base UI validation on staging because test requires top level group creation which is problematic
+ # on staging environment
+ RSpec.describe 'Manage', :requires_admin, except: { subdomain: :staging } do
describe 'Bulk project import' do
- let!(:staging?) { Runtime::Scenario.gitlab_address.include?('staging.gitlab.com') }
-
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
let(:admin_api_client) { Runtime::API::Client.as_admin }
let(:user) do
@@ -33,6 +33,7 @@ module QA
Resource::Project.fabricate_via_api! do |project|
project.api_client = api_client
project.group = source_group
+ project.initialize_with_readme = true
end
end
@@ -44,33 +45,87 @@ module QA
end
end
+ let(:imported_projects) do
+ imported_group.reload!.projects
+ end
+
+ let(:project_import_failures) do
+ imported_group.import_details
+ .find { |entity| entity[:destination_name] == source_project.name }
+ &.fetch(:failures)
+ end
+
before do
Runtime::Feature.enable(:bulk_import_projects)
- Runtime::Feature.enable(:top_level_group_creation_enabled) if staging?
sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
- source_project # fabricate source group and project
+ source_project.tap { |project| project.add_push_rules(member_check: true) } # fabricate source group and project
end
after do
user.remove_via_api!
ensure
Runtime::Feature.disable(:bulk_import_projects)
- Runtime::Feature.disable(:top_level_group_creation_enabled) if staging?
end
context 'with project' do
+ before do
+ imported_group # trigger import
+ end
+
it(
'successfully imports project',
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2297'
) do
expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
+ expect(imported_projects.count).to eq(1), "Expected to have 1 imported project"
- imported_projects = imported_group.reload!.projects
aggregate_failures do
- expect(imported_projects.count).to eq(1)
expect(imported_projects.first).to eq(source_project)
+ expect(project_import_failures).to be_empty, "Expected no errors, was: #{project_import_failures}"
+ end
+ end
+ end
+
+ context 'with project issues' do
+ let(:source_issue) do
+ Resource::Issue.fabricate_via_api! do |issue|
+ issue.api_client = api_client
+ issue.project = source_project
+ issue.labels = %w[label_one label_two]
+ end
+ end
+
+ let(:imported_issues) do
+ imported_projects.first.issues
+ end
+
+ let(:imported_issue) do
+ issue = imported_issues.first
+ Resource::Issue.init do |resource|
+ resource.api_client = api_client
+ resource.project = imported_projects.first
+ resource.iid = issue[:iid]
+ end
+ end
+
+ before do
+ source_issue # fabricate source group, project, issue
+ imported_group # trigger import
+ end
+
+ it(
+ 'successfully imports issue',
+ testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2325'
+ ) do
+ expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
+ expect(imported_projects.count).to eq(1), "Expected to have 1 imported project"
+
+ aggregate_failures do
+ expect(imported_issues.count).to eq(1)
+ expect(imported_issue.reload!).to eq(source_issue)
+ expect(project_import_failures).to be_empty, "Expected no errors, was: #{project_import_failures}"
end
end
end
diff --git a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb
index ec4f0387128..b85a0116f01 100644
--- a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb
+++ b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- context 'Gitaly automatic failover and recovery', :orchestrated, :gitaly_cluster, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238953', type: :flaky } do
+ context 'Gitaly automatic failover and recovery', :orchestrated, :gitaly_cluster do
# Variables shared between contexts. They're used and shared between
# contexts so they can't be `let` variables.
praefect_manager = Service::PraefectManager.new
diff --git a/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb
index 37670b70fd8..62437598f3b 100644
--- a/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb
+++ b/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb
@@ -3,7 +3,7 @@
module QA
RSpec.describe 'Create' do
context 'Gitaly' do
- describe 'Backend node recovery', :orchestrated, :gitaly_cluster, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/322647', type: :flaky } do
+ describe 'Backend node recovery', :orchestrated, :gitaly_cluster, :skip_live_env do
let(:praefect_manager) { Service::PraefectManager.new }
let(:project) do
Resource::Project.fabricate! do |project|
@@ -31,14 +31,6 @@ module QA
praefect_manager.stop_primary_node
praefect_manager.wait_for_gitaly_check
- # Confirm that we have access to the repo after failover
- Support::Waiter.wait_until(retry_on_exception: true, sleep_interval: 5) do
- Resource::Repository::Commit.fabricate_via_api! do |commits|
- commits.project = project
- commits.sha = project.default_branch
- end
- end
-
# Push a commit to the new primary
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
@@ -58,6 +50,11 @@ module QA
# Wait for automatic replication
praefect_manager.wait_for_replication(project.id)
+ # Force switch to the old primary node
+ # This ensures that the commit was replicated
+ praefect_manager.stop_secondary_node
+ praefect_manager.stop_tertiary_node
+
# Confirm that both commits are available
expect(project.commits.map { |commit| commit[:message].chomp })
.to include("Initial commit").and include("pushed after failover")
diff --git a/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb
index 1aea1bd1189..dfc2de02bf0 100644
--- a/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb
+++ b/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb
@@ -16,9 +16,15 @@ module QA
end
before do
+ praefect_manager.start_all_nodes
praefect_manager.wait_for_replication(project.id)
end
+ after do
+ # Leave the cluster in a suitable state for subsequent tests
+ praefect_manager.start_all_nodes
+ end
+
it 'reads from each node', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1264' do
pre_read_data = praefect_manager.query_read_distribution
@@ -42,9 +48,7 @@ module QA
after do
# Leave the cluster in a suitable state for subsequent tests
- praefect_manager.start_secondary_node
- praefect_manager.wait_for_health_check_all_nodes
- praefect_manager.wait_for_reliable_connection
+ praefect_manager.start_all_nodes
end
it 'does not read from the unhealthy node', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1263' do
diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb
new file mode 100644
index 00000000000..07ea7971396
--- /dev/null
+++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Create' do
+ context 'Praefect repository commands', :orchestrated, :gitaly_cluster do
+ let(:praefect_manager) { Service::PraefectManager.new }
+
+ let(:repo1) { { "relative_path" => "@hashed/repo1.git", "storage" => "gitaly1", "virtual_storage" => "default" } }
+ let(:repo2) { { "relative_path" => "@hashed/path/to/repo2.git", "storage" => "gitaly3", "virtual_storage" => "default" } }
+
+ before do
+ praefect_manager.add_repo_to_disk(praefect_manager.primary_node, repo1["relative_path"])
+ praefect_manager.add_repo_to_disk(praefect_manager.tertiary_node, repo2["relative_path"])
+ end
+
+ after do
+ praefect_manager.remove_repo_from_disk(repo1["relative_path"])
+ praefect_manager.remove_repo_from_disk(repo2["relative_path"])
+ praefect_manager.remove_repository_from_praefect_database(repo1["relative_path"])
+ praefect_manager.remove_repository_from_praefect_database(repo2["relative_path"])
+ end
+
+ it 'allows admin to manage difference between praefect database and disk state', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2344' do
+ # Some repos are on disk that praefect is not aware of
+ untracked_repositories = praefect_manager.list_untracked_repositories
+ expect(untracked_repositories).to include(repo1)
+ expect(untracked_repositories).to include(repo2)
+
+ # admin manually adds the first repo to the praefect database
+ praefect_manager.track_repository_in_praefect(repo1["relative_path"], repo1["storage"], repo1["virtual_storage"])
+ untracked_repositories = praefect_manager.list_untracked_repositories
+ expect(untracked_repositories).not_to include(repo1)
+ expect(untracked_repositories).to include(repo2)
+ expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])).to be true
+ expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be true
+
+ # admin manually adds the second repo to the praefect database
+ praefect_manager.track_repository_in_praefect(repo2["relative_path"], repo2["storage"], repo2["virtual_storage"])
+ untracked_repositories = praefect_manager.list_untracked_repositories
+ expect(untracked_repositories).not_to include(repo2)
+ expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo2["relative_path"])).to be true
+ expect(praefect_manager.praefect_database_tracks_repo?(repo2["relative_path"])).to be true
+
+ # admin ensures replication to other nodes occurs
+ expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo1["relative_path"])).to be true
+ expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.tertiary_node, repo1["relative_path"])).to be true
+ expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.primary_node, repo2["relative_path"])).to be true
+ expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo2["relative_path"])).to be true
+
+ # admin chooses to remove the first repo completely from praefect and disk
+ praefect_manager.remove_tracked_praefect_repository(repo1["relative_path"], repo1["virtual_storage"])
+ expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])).to be false
+ expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.secondary_node, repo1["relative_path"])).to be false
+ expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo1["relative_path"])).to be false
+ expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be false
+
+ untracked_repositories = praefect_manager.list_untracked_repositories
+ expect(untracked_repositories).not_to include(repo1)
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
index c136d14c1e5..15d51c14d26 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/bulk_import_group_spec.rb
@@ -4,17 +4,16 @@ module QA
RSpec.describe 'Manage', :requires_admin do
describe 'Bulk group import' do
let!(:staging?) { Runtime::Scenario.gitlab_address.include?('staging.gitlab.com') }
-
- let(:admin_api_client) { Runtime::API::Client.as_admin }
- let(:user) do
+ let!(:admin_api_client) { Runtime::API::Client.as_admin }
+ let!(:user) do
Resource::User.fabricate_via_api! do |usr|
usr.api_client = admin_api_client
usr.hard_delete_on_api_removal = true
end
end
- let(:api_client) { Runtime::API::Client.new(user: user) }
- let(:personal_access_token) { api_client.personal_access_token }
+ let!(:api_client) { Runtime::API::Client.new(user: user) }
+ let!(:personal_access_token) { api_client.personal_access_token }
let(:sandbox) do
Resource::Sandbox.fabricate_via_api! do |group|
@@ -23,7 +22,7 @@ module QA
end
let(:source_group) do
- Resource::Sandbox.fabricate_via_api! do |group|
+ Resource::Sandbox.fabricate! do |group|
group.api_client = api_client
group.path = "source-group-for-import-#{SecureRandom.hex(4)}"
end
@@ -38,14 +37,12 @@ module QA
end
before do
- Runtime::Feature.enable(:top_level_group_creation_enabled) if staging?
-
sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER)
- # create groups explicitly before connecting gitlab instance
+ Flow::Login.sign_in(as: user)
+
source_group
- Flow::Login.sign_in(as: user)
Page::Main::Menu.perform(&:go_to_create_group)
Page::Group::New.perform do |group|
group.switch_to_import_tab
@@ -53,6 +50,10 @@ module QA
end
end
+ after do
+ user.remove_via_api!
+ end
+
it(
'imports group from UI',
testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1806',
@@ -72,12 +73,6 @@ module QA
end
end
end
-
- after do
- user.remove_via_api!
- ensure
- Runtime::Feature.disable(:top_level_group_creation_enabled) if staging?
- end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
index ca95d567316..9625771164c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :smoke do
+ RSpec.describe 'Manage', :smoke, :mobile do
describe 'basic user login' do
it 'user logs in using basic credentials and logs out', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1578' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
index 2b1c956039f..734529f319a 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :mixed_env, :smoke, only: { subdomain: :staging } do
+ RSpec.describe 'Manage', only: { subdomain: :staging }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/344213', type: :stale } do
describe 'basic user' do
it 'remains logged in when redirected from canary to non-canary node', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2251' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index af4e7126c29..34a7431e328 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -5,13 +5,13 @@ module QA
describe 'Project', :requires_admin do
shared_examples 'successful project creation' do
it 'creates a new project' do
- Page::Project::Show.perform do |project|
- expect(project).to have_content(project_name)
- expect(project).to have_content(
+ Page::Project::Show.perform do |project_page|
+ expect(project_page).to have_content(project_name)
+ expect(project_page).to have_content(
/Project \S?#{project_name}\S+ was successfully created/
)
- expect(project).to have_content('create awesome project test')
- expect(project).to have_content('The repository for this project is empty')
+ expect(project_page).to have_content('create awesome project test')
+ expect(project_page).to have_content('The repository for this project is empty')
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index 4e3739a7672..c078c4bf12e 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -4,7 +4,6 @@ module QA
RSpec.describe 'Manage', :github, :requires_admin do
describe 'Project import' do
let(:github_repo) { 'gitlab-qa-github/test-project' }
- let(:imported_project_name) { 'imported-project' }
let(:api_client) { Runtime::API::Client.as_admin }
let(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
let(:user) do
@@ -17,11 +16,10 @@ module QA
let(:imported_project) do
Resource::ProjectImportedFromGithub.init do |project|
project.import = true
- project.add_name_uuid = false
- project.name = imported_project_name
project.group = group
project.github_personal_access_token = Runtime::Env.github_access_token
project.github_repository_path = github_repo
+ project.api_client = api_client
end
end
@@ -43,7 +41,7 @@ module QA
it 'imports a GitHub repo', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1607' do
Page::Project::Import::Github.perform do |import_page|
import_page.add_personal_access_token(Runtime::Env.github_access_token)
- import_page.import!(github_repo, group.full_path, imported_project_name)
+ import_page.import!(github_repo, group.full_path, imported_project.name)
aggregate_failures do
expect(import_page).to have_imported_project(github_repo)
@@ -56,7 +54,7 @@ module QA
imported_project.reload!.visit!
Page::Project::Show.perform do |project|
aggregate_failures do
- expect(project).to have_content(imported_project_name)
+ expect(project).to have_content(imported_project.name)
expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.')
end
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
index 7519f4daae2..81ae8b82ef6 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb
@@ -9,7 +9,7 @@ module QA
Flow::Login.sign_in
end
- it 'creates an issue', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1185' do
+ it 'creates an issue', :mobile, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1185' do
issue = Resource::Issue.fabricate_via_browser_ui!
Page::Project::Menu.perform(&:click_issues)
@@ -19,13 +19,13 @@ module QA
end
end
- it 'closes an issue', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1222' do
+ it 'closes an issue', :mobile, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1222' do
closed_issue.visit!
Page::Project::Issue::Show.perform do |issue_page|
issue_page.click_close_issue_button
- expect(issue_page).to have_element(:reopen_issue_button)
+ expect(issue_page).to have_reopen_issue_button
end
Page::Project::Menu.perform(&:click_issues)
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb
index 1752513a831..96c9c9b55b4 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/batch_suggestion_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- context 'Add batch suggestions to a Merge Request', :transient do
+ context 'Add batch suggestions to a Merge Request' do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'suggestions_project'
@@ -46,13 +46,13 @@ module QA
merge_request.visit!
end
- it 'applies multiple suggestions', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1838', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/342131', type: :stale } do
+ it 'applies multiple suggestions', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1838' do
Page::MergeRequest::Show.perform do |merge_request|
merge_request.click_diffs_tab
4.times { merge_request.add_suggestion_to_batch }
merge_request.apply_suggestion_with_message("Custom commit message")
- expect(merge_request).to have_css('.badge-success', text: "Applied", count: 4)
+ expect(merge_request).to have_suggestions_applied(4)
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb
index 339010cd1df..719006e87eb 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb
@@ -48,7 +48,7 @@ module QA
merge_request.click_diffs_tab
merge_request.apply_suggestion_with_message(commit_message)
- expect(merge_request).to have_css('.badge-success', text: 'Applied')
+ expect(merge_request).to have_suggestions_applied
merge_request.click_commits_tab
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb
index c648fecf847..158e841514c 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/link_to_line_in_web_ide_spec.rb
@@ -21,7 +21,8 @@ module QA
it 'can link to a specific line of code in Web IDE', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1844' do
project.visit!
- Page::Project::Show.perform(&:open_web_ide!)
+ # Open Web IDE by using a keyboard shortcut
+ Page::Project::Show.perform(&:open_web_ide_via_shortcut)
Page::Project::WebIDE::Edit.perform do |ide|
ide.select_file('app.js')
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
index 47117ae751f..e6910ad8592 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb
@@ -1,10 +1,9 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Verify', :runner do
+ RSpec.describe 'Verify', :smoke, :runner do
describe 'Pipeline creation and processing' do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
- let(:max_wait) { 30 }
let(:project) do
Resource::Project.fabricate_via_api! do |project|
@@ -21,11 +20,10 @@ module QA
end
after do
- runner.remove_via_api!
+ [runner, project].each(&:remove_via_api!)
end
it 'users creates a pipeline which gets processed', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1601' do
- # TODO: Convert back to :smoke once proved to be stable. Related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/300909
Flow::Login.sign_in
Resource::Repository::Commit.fabricate_via_api! do |commit|
@@ -68,19 +66,21 @@ module QA
Flow::Pipeline.visit_latest_pipeline
- {
- 'test-success': :passed,
- 'test-failure': :failed,
- 'test-tags-mismatch': :pending,
- 'test-artifacts': :passed
- }.each do |job, status|
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job(job)
- end
+ aggregate_failures do
+ {
+ 'test-success': 'passed',
+ 'test-failure': 'failed',
+ 'test-tags-mismatch': 'pending',
+ 'test-artifacts': 'passed'
+ }.each do |job, status|
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job(job)
+ end
- Page::Project::Job::Show.perform do |show|
- expect(show).to public_send("be_#{status}")
- show.click_element(:pipeline_path, Page::Project::Pipeline::Show)
+ Page::Project::Job::Show.perform do |show|
+ expect(show).to have_status(status), "Expected job status to be #{status} but got #{show.status_badge} instead."
+ show.click_element(:pipeline_path, Page::Project::Pipeline::Show)
+ end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb
new file mode 100644
index 00000000000..51735d79fbd
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb
@@ -0,0 +1,191 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Package', :orchestrated, only: { pipeline: :main } do
+ describe 'Self-managed Container Registry' do
+ using RSpec::Parameterized::TableSyntax
+
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'project-with-registry'
+ project.template_name = 'express'
+ project.visibility = :private
+ end
+ end
+
+ let(:project_deploy_token) do
+ Resource::DeployToken.fabricate_via_browser_ui! do |deploy_token|
+ deploy_token.name = 'registry-deploy-token'
+ deploy_token.project = project
+ deploy_token.scopes = [
+ :read_repository,
+ :read_package_registry,
+ :write_package_registry,
+ :read_registry,
+ :write_registry
+ ]
+ end
+ end
+
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = "qa-runner-#{Time.now.to_i}"
+ runner.tags = ["runner-for-#{project.name}"]
+ runner.executor = :docker
+ runner.project = project
+ end
+ end
+
+ let(:personal_access_token) { Runtime::Env.personal_access_token }
+
+ before do
+ Flow::Login.sign_in
+ project.visit!
+ end
+
+ after do
+ runner.remove_via_api!
+ project.remove_via_api!
+ end
+
+ where(:authentication_token_type, :token_name) do
+ :personal_access_token | 'Personal Access Token'
+ :project_deploy_token | 'Deploy Token'
+ :ci_job_token | 'Job Token'
+ end
+
+ with_them do
+ let(:auth_token) do
+ case authentication_token_type
+ when :personal_access_token
+ "\"#{personal_access_token}\""
+ when :project_deploy_token
+ "\"#{project_deploy_token.password}\""
+ when :ci_job_token
+ '$CI_JOB_TOKEN'
+ end
+ end
+
+ let(:auth_user) do
+ case authentication_token_type
+ when :personal_access_token
+ "$CI_REGISTRY_USER"
+ when :project_deploy_token
+ "\"#{project_deploy_token.username}\""
+ when :ci_job_token
+ 'gitlab-ci-token'
+ end
+ end
+
+ context "when tls is disabled" do
+ it "using a #{params[:token_name]}, pushes image and deletes tag", :registry do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([{
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ build:
+ image: docker:19.03.12
+ stage: build
+ services:
+ - name: docker:19.03.12-dind
+ command: ["--insecure-registry=gitlab.test:5050"]
+ variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ script:
+ - docker login -u #{auth_user} -p #{auth_token} gitlab.test:5050
+ - docker build -t $IMAGE_TAG .
+ - docker push $IMAGE_TAG
+ tags:
+ - "runner-for-#{project.name}"
+ YAML
+ }])
+ end
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('build')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+
+ Page::Project::Menu.perform(&:go_to_container_registry)
+
+ Page::Project::Registry::Show.perform do |registry|
+ expect(registry).to have_registry_repository(project.path_with_namespace)
+
+ registry.click_on_image(project.path_with_namespace)
+ expect(registry).to have_tag('master')
+
+ registry.click_delete
+ expect(registry).not_to have_tag('master')
+ end
+ end
+ end
+ end
+
+ context "when tls is enabled" do
+ it "pushes image and deletes tag", :registry_tls, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2378' do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([{
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ build:
+ image: docker:19.03.12
+ stage: build
+ services:
+ - name: docker:19.03.12-dind
+ command:
+ - /bin/sh
+ - -c
+ - |
+ apk add --no-cache openssl
+ true | openssl s_client -showcerts -connect gitlab.test:5050 > /usr/local/share/ca-certificates/gitlab.test.crt
+ update-ca-certificates
+ dockerd-entrypoint.sh || exit
+ variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ script:
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD gitlab.test:5050
+ - docker build -t $IMAGE_TAG .
+ - docker push $IMAGE_TAG
+ tags:
+ - "runner-for-#{project.name}"
+ YAML
+ }])
+ end
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('build')
+ end
+
+ Support::Retrier.retry_until(max_duration: 800, sleep_interval: 10) do
+ project.pipelines.last[:status] == 'success'
+ end
+
+ Page::Project::Menu.perform(&:go_to_container_registry)
+
+ Page::Project::Registry::Show.perform do |registry|
+ expect(registry).to have_registry_repository(project.path_with_namespace)
+
+ registry.click_on_image(project.path_with_namespace)
+ expect(registry).to have_tag('master')
+
+ registry.click_delete
+ expect(registry).not_to have_tag('master')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb
index 65519cdebec..65519cdebec 100644
--- a/qa/qa/specs/features/browser_ui/5_package/container_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb
diff --git a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/online_garbage_collection_spec.rb
index 3ec76e8afad..82b7af8eba7 100644
--- a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/online_garbage_collection_spec.rb
@@ -2,7 +2,8 @@
module QA
RSpec.describe 'Package' do
- describe 'Container Registry Online Garbage Collection', :registry_gc, only: { subdomain: %i[pre] } do
+ # TODO: Remove :requires_admin when the `Runtime::Feature.enable` method call is removed
+ describe 'Container Registry Online Garbage Collection', :registry_gc, :requires_admin, only: { subdomain: %i[pre] } do
let(:group) { Resource::Group.fabricate_via_api! }
let(:imported_project) do
@@ -23,12 +24,12 @@ module QA
STAGE_THREE_VALIDATION_DELAY: "6m"
STAGE_FOUR_VALIDATION_DELAY: "12m"
STAGE_FIVE_VALIDATION_DELAY: "12m"
-
+
stages:
- generate
- build
- test
-
+
.base: &base
image: docker:19
services:
@@ -39,11 +40,11 @@ module QA
DOCKER_TLS_VERIFY: 1
DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client"
before_script:
- - until docker info; do sleep 1; done
+ - until docker info; do sleep 1; done
- mkdir -p $GOPATH
- mkdir -p $BUILD_CACHE
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
-
+
test:
stage: generate
extends: .base
@@ -64,6 +65,8 @@ module QA
end
before do
+ Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation)
+
Flow::Login.sign_in
imported_project
diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb
deleted file mode 100644
index 3d02c2884a2..00000000000
--- a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb
+++ /dev/null
@@ -1,88 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- RSpec.describe 'Package', :registry, :orchestrated, only: { pipeline: :main } do
- describe 'Self-managed Container Registry' do
- let(:project) do
- Resource::Project.fabricate_via_api! do |project|
- project.name = 'project-with-registry'
- project.template_name = 'express'
- end
- end
-
- let!(:runner) do
- Resource::Runner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.name}"]
- runner.executor = :docker
- runner.project = project
- end
- end
-
- before do
- Flow::Login.sign_in
- project.visit!
- end
-
- after do
- runner.remove_via_api!
- end
-
- it "pushes image and deletes tag", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1911' do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([{
- file_path: '.gitlab-ci.yml',
- content:
- <<~YAML
- build:
- image: docker:19.03.12
- stage: build
- services:
- - name: docker:19.03.12-dind
- command:
- - /bin/sh
- - -c
- - |
- apk add --no-cache openssl
- true | openssl s_client -showcerts -connect gitlab.test:5050 > /usr/local/share/ca-certificates/gitlab.test.crt
- update-ca-certificates
- dockerd-entrypoint.sh || exit
- variables:
- IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
- script:
- - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD gitlab.test:5050
- - docker build -t $IMAGE_TAG .
- - docker push $IMAGE_TAG
- tags:
- - "runner-for-#{project.name}"
- YAML
- }])
- end
-
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('build')
- end
-
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
-
- Page::Project::Menu.perform(&:go_to_container_registry)
-
- Page::Project::Registry::Show.perform do |registry|
- expect(registry).to have_registry_repository(project.path_with_namespace)
-
- registry.click_on_image(project.path_with_namespace)
- expect(registry).to have_tag('master')
-
- registry.click_delete
- expect(registry).not_to have_tag('master')
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb
index ea7f7cc1c05..b941d5434df 100644
--- a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Package', :orchestrated, :registry do
+ RSpec.describe 'Package', :orchestrated, :registry, only: { pipeline: :main } do
describe 'Dependency Proxy' do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
@@ -22,6 +22,7 @@ module QA
let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" }
let(:dependency_proxy_url) { "#{gitlab_host_with_port}/#{project.group.full_path}/dependency_proxy/containers" }
+ let(:image_sha) { 'alpine@sha256:c3d45491770c51da4ef58318e3714da686bc7165338b7ab5ac758e75c7455efb' }
before do
Flow::Login.sign_in
@@ -56,22 +57,16 @@ module QA
image: "#{docker_client_version}"
services:
- name: "#{docker_client_version}-dind"
- command:
- - /bin/sh
- - -c
- - |
- apk add --no-cache openssl
- true | openssl s_client -showcerts -connect gitlab.test:5050 > /usr/local/share/ca-certificates/gitlab.test.crt
- update-ca-certificates
- dockerd-entrypoint.sh || exit
+ command: ["--insecure-registry=gitlab.test:80"]
before_script:
- apk add curl jq grep
- - docker login -u "$CI_DEPENDENCY_PROXY_USER" -p "$CI_DEPENDENCY_PROXY_PASSWORD" "$CI_DEPENDENCY_PROXY_SERVER"
+ - echo $CI_DEPENDENCY_PROXY_SERVER
+ - docker login -u "$CI_DEPENDENCY_PROXY_USER" -p "$CI_DEPENDENCY_PROXY_PASSWORD" gitlab.test:80
script:
- - docker pull #{dependency_proxy_url}/alpine:latest
+ - docker pull #{dependency_proxy_url}/#{image_sha}
- TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq --raw-output .token)
- 'curl --head --header "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" 2>&1'
- - docker pull #{dependency_proxy_url}/alpine:latest
+ - docker pull #{dependency_proxy_url}/#{image_sha}
- 'curl --head --header "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" 2>&1'
tags:
- "runner-for-#{project.name}"
@@ -95,7 +90,7 @@ module QA
Page::Group::Menu.perform(&:go_to_dependency_proxy)
Page::Group::DependencyProxy.perform do |index|
- expect(index).to have_blob_count("Contains 2 blobs of images")
+ expect(index).to have_blob_count("Contains 1 blobs of images")
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb
deleted file mode 100644
index bf1d2a04dba..00000000000
--- a/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb
+++ /dev/null
@@ -1,340 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- RSpec.describe 'Package', :orchestrated, :packages, :reliable, :object_storage do
- describe 'Maven Repository' do
- include Runtime::Fixtures
-
- let(:group_id) { 'com.gitlab.qa' }
- let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
- let(:another_artifact_id) { "maven-#{SecureRandom.hex(8)}" }
- let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
- let(:auth_token) do
- unless Page::Main::Menu.perform(&:signed_in?)
- Flow::Login.sign_in
- end
-
- Resource::PersonalAccessToken.fabricate!.token
- end
-
- let(:project) do
- Resource::Project.fabricate_via_api! do |project|
- project.name = 'maven-package-project'
- end
- end
-
- let(:another_project) do
- Resource::Project.fabricate_via_api! do |another_project|
- another_project.name = 'another-maven-package-project'
- another_project.group = project.group
- end
- end
-
- let(:package) do
- Resource::Package.init do |package|
- package.name = package_name
- package.project = project
- end
- end
-
- let!(:runner) do
- Resource::Runner.fabricate! do |runner|
- runner.name = "qa-runner-#{Time.now.to_i}"
- runner.tags = ["runner-for-#{project.group.name}"]
- runner.executor = :docker
- runner.token = project.group.runners_token
- end
- end
-
- let!(:gitlab_address_with_port) do
- uri = URI.parse(Runtime::Scenario.gitlab_address)
- "#{uri.scheme}://#{uri.host}:#{uri.port}"
- end
-
- let(:pom_xml) do
- {
- file_path: 'pom.xml',
- content: <<~XML
- <project>
- <groupId>#{group_id}</groupId>
- <artifactId>#{artifact_id}</artifactId>
- <version>1.0</version>
- <modelVersion>4.0.0</modelVersion>
- <repositories>
- <repository>
- <id>#{project.name}</id>
- <url>#{gitlab_address_with_port}/api/v4/groups/#{project.group.id}/-/packages/maven</url>
- </repository>
- </repositories>
- <distributionManagement>
- <repository>
- <id>#{project.name}</id>
- <url>#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/maven</url>
- </repository>
- <snapshotRepository>
- <id>#{project.name}</id>
- <url>#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/maven</url>
- </snapshotRepository>
- </distributionManagement>
- </project>
- XML
- }
- end
-
- let(:pom_xml_another_project) do
- {
- file_path: 'pom.xml',
- content: <<~XML
- <project>
- <groupId>#{group_id}</groupId>
- <artifactId>#{another_artifact_id}</artifactId>
- <version>1.0</version>
- <modelVersion>4.0.0</modelVersion>
- <repositories>
- <repository>
- <id>#{another_project.name}</id>
- <url>#{gitlab_address_with_port}/api/v4/groups/#{another_project.group.id}/-/packages/maven</url>
- </repository>
- </repositories>
- <distributionManagement>
- <repository>
- <id>#{another_project.name}</id>
- <url>#{gitlab_address_with_port}/api/v4/projects/#{another_project.id}/packages/maven</url>
- </repository>
- <snapshotRepository>
- <id>#{another_project.name}</id>
- <url>#{gitlab_address_with_port}/api/v4/projects/#{another_project.id}/packages/maven</url>
- </snapshotRepository>
- </distributionManagement>
- <dependencies>
- <dependency>
- <groupId>#{group_id}</groupId>
- <artifactId>#{artifact_id}</artifactId>
- <version>1.0</version>
- </dependency>
- </dependencies>
- </project>
- XML
- }
- end
-
- let(:settings_xml) do
- {
- file_path: 'settings.xml',
- content: <<~XML
- <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
- xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
- <servers>
- <server>
- <id>#{project.name}</id>
- <configuration>
- <httpHeaders>
- <property>
- <name>Private-Token</name>
- <value>#{auth_token}</value>
- </property>
- </httpHeaders>
- </configuration>
- </server>
- </servers>
- </settings>
- XML
- }
- end
-
- let(:gitlab_ci_deploy_yml) do
- {
- file_path: '.gitlab-ci.yml',
- content:
- <<~YAML
- deploy:
- image: maven:3.6-jdk-11
- script:
- - 'mvn deploy -s settings.xml'
- - "mvn dependency:get -Dartifact=#{group_id}:#{artifact_id}:1.0"
- only:
- - "#{project.default_branch}"
- tags:
- - "runner-for-#{project.group.name}"
- YAML
- }
- end
-
- let(:gitlab_ci_install_yml) do
- {
- file_path: '.gitlab-ci.yml',
- content:
- <<~YAML
- install:
- image: maven:3.6-jdk-11
- script:
- - "mvn install"
- only:
- - "#{project.default_branch}"
- tags:
- - "runner-for-#{another_project.group.name}"
- YAML
- }
- end
-
- after do
- runner.remove_via_api!
- project.remove_via_api!
- another_project.remove_via_api!
- end
-
- it 'pushes and pulls a Maven package via CI and deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1627' do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- gitlab_ci_deploy_yml,
- settings_xml,
- pom_xml
- ])
- end
-
- project.visit!
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('deploy')
- end
-
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
-
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = another_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- gitlab_ci_install_yml,
- pom_xml_another_project
- ])
- end
-
- another_project.visit!
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('install')
- end
-
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
-
- project.visit!
-
- Page::Project::Menu.perform(&:click_packages_link)
-
- Page::Project::Packages::Index.perform do |index|
- expect(index).to have_package(package_name)
-
- index.click_package(package_name)
- end
-
- Page::Project::Packages::Show.perform do |show|
- expect(show).to have_package_info(package_name, "1.0")
- show.click_delete
- end
-
- Page::Project::Packages::Index.perform do |index|
- expect(index).to have_content("Package deleted successfully")
- expect(index).not_to have_package(package_name)
- end
- end
-
- context 'when "allow duplicate" setting is disabled' do
- before do
- Flow::Login.sign_in
-
- project.group.visit!
-
- Page::Group::Menu.perform(&:go_to_package_settings)
- Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled)
- end
-
- it 'prevents users from publishing duplicate Maven packages at the group level', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1830' do
- with_fixtures([pom_xml, settings_xml]) do |dir|
- Service::DockerRun::Maven.new(dir).publish!
- end
-
- project.visit!
- Page::Project::Menu.perform(&:click_packages_link)
-
- Page::Project::Packages::Index.perform do |index|
- expect(index).to have_package(package_name)
- end
-
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = another_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- gitlab_ci_deploy_yml,
- settings_xml,
- pom_xml
- ])
- end
-
- another_project.visit!
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('deploy')
- end
-
- Page::Project::Job::Show.perform do |job|
- expect(job).not_to be_successful(timeout: 800)
- end
- end
- end
-
- context 'when "allow duplicate" setting is enabled' do
- before do
- Flow::Login.sign_in
-
- project.group.visit!
-
- Page::Group::Menu.perform(&:go_to_package_settings)
- Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled)
- end
-
- it 'allows users to publish duplicate Maven packages at the group level', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1829' do
- with_fixtures([pom_xml, settings_xml]) do |dir|
- Service::DockerRun::Maven.new(dir).publish!
- end
-
- project.visit!
- Page::Project::Menu.perform(&:click_packages_link)
-
- Page::Project::Packages::Index.perform do |index|
- expect(index).to have_package(package_name)
- end
-
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = another_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- gitlab_ci_deploy_yml,
- settings_xml,
- pom_xml
- ])
- end
-
- another_project.visit!
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('deploy')
- end
-
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/5_package/composer_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb
index 9ddf485870d..9ddf485870d 100644
--- a/qa/qa/specs/features/browser_ui/5_package/composer_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb
diff --git a/qa/qa/specs/features/browser_ui/5_package/conan_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb
index a8f1fc2a7de..126be22d760 100644
--- a/qa/qa/specs/features/browser_ui/5_package/conan_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb
@@ -17,7 +17,7 @@ module QA
let(:package) do
Resource::Package.init do |package|
- package.name = 'conantest'
+ package.name = "conantest-#{SecureRandom.hex(8)}"
package.project = project
end
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/generic_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb
index 2e5fa2c2904..86aca120eed 100644
--- a/qa/qa/specs/features/browser_ui/5_package/generic_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb
@@ -11,7 +11,7 @@ module QA
let(:package) do
Resource::Package.init do |package|
- package.name = "my_package"
+ package.name = "my_package-#{SecureRandom.hex(8)}"
package.project = project
end
end
@@ -36,13 +36,13 @@ module QA
upload:
stage: upload
script:
- - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file file.txt ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/my_package/0.0.1/file.txt'
+ - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file file.txt ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/#{package.name}/0.0.1/file.txt'
tags:
- "runner-for-#{project.name}"
download:
stage: download
script:
- - 'wget --header="JOB-TOKEN: $CI_JOB_TOKEN" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/my_package/0.0.1/file.txt -O file_downloaded.txt'
+ - 'wget --header="JOB-TOKEN: $CI_JOB_TOKEN" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/#{package.name}/0.0.1/file.txt -O file_downloaded.txt'
tags:
- "runner-for-#{project.name}"
YAML
diff --git a/qa/qa/specs/features/browser_ui/5_package/helm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb
index fe52fd03ad8..3f5e8b1a630 100644
--- a/qa/qa/specs/features/browser_ui/5_package/helm_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb
@@ -6,7 +6,7 @@ module QA
include Runtime::Fixtures
include_context 'packages registry qa scenario'
- let(:package_name) { 'gitlab_qa_helm' }
+ let(:package_name) { "gitlab_qa_helm-#{SecureRandom.hex(8)}" }
let(:package_version) { '1.3.7' }
let(:package_type) { 'helm' }
diff --git a/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb
index ec9feca84b9..2aa93de0b9e 100644
--- a/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb
@@ -8,7 +8,7 @@ module QA
include_context 'packages registry qa scenario'
let(:group_id) { 'com.gitlab.qa' }
- let(:artifact_id) { 'maven_gradle' }
+ let(:artifact_id) { "maven_gradle-#{SecureRandom.hex(8)}" }
let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
let(:package_version) { '1.3.7' }
let(:package_type) { 'maven_gradle' }
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb
new file mode 100644
index 00000000000..f42093bffcd
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb
@@ -0,0 +1,317 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Package', :orchestrated, :packages, :object_storage do
+ describe 'Maven Repository' do
+ using RSpec::Parameterized::TableSyntax
+ include Runtime::Fixtures
+ include_context 'packages registry qa scenario'
+
+ let(:group_id) { 'com.gitlab.qa' }
+ let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
+ let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
+ let(:package_version) { '1.3.7' }
+ let(:package_type) { 'maven' }
+
+ let(:package_gitlab_ci_file) do
+ {
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ deploy:
+ image: maven:3.6-jdk-11
+ script:
+ - 'mvn deploy -s settings.xml'
+ only:
+ - "#{package_project.default_branch}"
+ tags:
+ - "runner-for-#{package_project.group.name}"
+ YAML
+ }
+ end
+
+ let(:package_pom_file) do
+ {
+ file_path: 'pom.xml',
+ content: <<~XML
+ <project>
+ <groupId>#{group_id}</groupId>
+ <artifactId>#{artifact_id}</artifactId>
+ <version>#{package_version}</version>
+ <modelVersion>4.0.0</modelVersion>
+ <repositories>
+ <repository>
+ <id>#{package_project.name}</id>
+ <url>#{gitlab_address_with_port}/api/v4/groups/#{package_project.group.id}/-/packages/maven</url>
+ </repository>
+ </repositories>
+ <distributionManagement>
+ <repository>
+ <id>#{package_project.name}</id>
+ <url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url>
+ </repository>
+ <snapshotRepository>
+ <id>#{package_project.name}</id>
+ <url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url>
+ </snapshotRepository>
+ </distributionManagement>
+ </project>
+ XML
+ }
+ end
+
+ let(:client_gitlab_ci_file) do
+ {
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ install:
+ image: maven:3.6-jdk-11
+ script:
+ - "mvn install -s settings.xml"
+ only:
+ - "#{client_project.default_branch}"
+ tags:
+ - "runner-for-#{client_project.group.name}"
+ YAML
+ }
+ end
+
+ let(:client_pom_file) do
+ {
+ file_path: 'pom.xml',
+ content: <<~XML
+ <project>
+ <groupId>#{group_id}</groupId>
+ <artifactId>maven_client</artifactId>
+ <version>1.0</version>
+ <modelVersion>4.0.0</modelVersion>
+ <repositories>
+ <repository>
+ <id>#{package_project.name}</id>
+ <url>#{gitlab_address_with_port}/api/v4/groups/#{package_project.group.id}/-/packages/maven</url>
+ </repository>
+ </repositories>
+ <dependencies>
+ <dependency>
+ <groupId>#{group_id}</groupId>
+ <artifactId>#{artifact_id}</artifactId>
+ <version>#{package_version}</version>
+ </dependency>
+ </dependencies>
+ </project>
+ XML
+ }
+ end
+
+ let(:settings_xml_with_pat) do
+ {
+ file_path: 'settings.xml',
+ content: <<~XML
+ <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
+ <servers>
+ <server>
+ <id>#{package_project.name}</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>Private-Token</name>
+ <value>#{personal_access_token}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+ </settings>
+ XML
+ }
+ end
+
+ where(:authentication_token_type, :maven_header_name) do
+ :personal_access_token | 'Private-Token'
+ :ci_job_token | 'Job-Token'
+ :project_deploy_token | 'Deploy-Token'
+ end
+
+ with_them do
+ let(:token) do
+ case authentication_token_type
+ when :personal_access_token
+ personal_access_token
+ when :ci_job_token
+ '${env.CI_JOB_TOKEN}'
+ when :project_deploy_token
+ project_deploy_token.password
+ end
+ end
+
+ let(:settings_xml) do
+ {
+ file_path: 'settings.xml',
+ content: <<~XML
+ <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd">
+ <servers>
+ <server>
+ <id>#{package_project.name}</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>#{maven_header_name}</name>
+ <value>#{token}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+ </settings>
+ XML
+ }
+ end
+
+ it "pushes and pulls a maven package via maven using #{params[:authentication_token_type]}" do
+ # pushing
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = package_project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([
+ package_gitlab_ci_file,
+ package_pom_file,
+ settings_xml
+ ])
+ end
+
+ package_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('deploy')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+
+ Page::Project::Menu.perform(&:click_packages_link)
+
+ Page::Project::Packages::Index.perform do |index|
+ expect(index).to have_package(package_name)
+
+ index.click_package(package_name)
+ end
+
+ Page::Project::Packages::Show.perform do |show|
+ expect(show).to have_package_info(package_name, package_version)
+ end
+
+ # pulling
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = client_project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([
+ client_gitlab_ci_file,
+ client_pom_file,
+ settings_xml
+ ])
+ end
+
+ client_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('install')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+ end
+
+ context 'duplication setting' do
+ before do
+ package_project.group.visit!
+
+ Page::Group::Menu.perform(&:go_to_package_settings)
+ end
+
+ context 'when disabled' do
+ before do
+ Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled)
+ end
+
+ it "prevents users from publishing group level Maven packages duplicates using #{params[:authentication_token_type]}" do
+ create_duplicated_package
+
+ push_duplicated_package
+
+ client_project.visit!
+
+ show_latest_deploy_job
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).not_to be_successful(timeout: 800)
+ end
+ end
+ end
+
+ context 'when enabled' do
+ before do
+ Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled)
+ end
+
+ it "allows users to publish group level Maven packages duplicates using #{params[:authentication_token_type]}" do
+ create_duplicated_package
+
+ push_duplicated_package
+
+ show_latest_deploy_job
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+ end
+ end
+
+ def create_duplicated_package
+ with_fixtures([package_pom_file, settings_xml_with_pat]) do |dir|
+ Service::DockerRun::Maven.new(dir).publish!
+ end
+
+ package_project.visit!
+
+ Page::Project::Menu.perform(&:click_packages_link)
+
+ Page::Project::Packages::Index.perform do |index|
+ expect(index).to have_package(package_name)
+ end
+ end
+
+ def push_duplicated_package
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = client_project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([
+ package_gitlab_ci_file,
+ package_pom_file,
+ settings_xml
+ ])
+ end
+ end
+
+ def show_latest_deploy_job
+ client_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('deploy')
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb
index 5a3b4388f0c..f2b1c1b01a0 100644
--- a/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb
@@ -1,8 +1,8 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Package', :orchestrated, :packages, :reliable, :object_storage do
- describe 'npm registry' do
+ RSpec.describe 'Package Registry', :orchestrated, :packages, :reliable, :object_storage do
+ describe 'npm instance level endpoint' do
using RSpec::Parameterized::TableSyntax
include Runtime::Fixtures
@@ -19,6 +19,11 @@ module QA
Resource::DeployToken.fabricate_via_browser_ui! do |deploy_token|
deploy_token.name = 'npm-deploy-token'
deploy_token.project = project
+ deploy_token.scopes = [
+ :read_repository,
+ :read_package_registry,
+ :write_package_registry
+ ]
end
end
@@ -28,13 +33,13 @@ module QA
let!(:project) do
Resource::Project.fabricate_via_api! do |project|
- project.name = 'npm-project'
+ project.name = 'npm-instace-level-publish'
end
end
let!(:another_project) do
Resource::Project.fabricate_via_api! do |another_project|
- another_project.name = 'npm-another-project'
+ another_project.name = 'npm-instance-level-install'
another_project.template_name = 'express'
another_project.group = project.group
end
@@ -54,7 +59,7 @@ module QA
file_path: '.gitlab-ci.yml',
content:
<<~YAML
- image: node:14-buster
+ image: node:latest
stages:
- deploy
@@ -62,6 +67,7 @@ module QA
deploy:
stage: deploy
script:
+ - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=#{auth_token}">.npmrc
- npm publish
only:
- "#{project.default_branch}"
@@ -119,7 +125,7 @@ module QA
let(:package) do
Resource::Package.init do |package|
- package.name = "@#{registry_scope}/#{project.name}"
+ package.name = "@#{registry_scope}/#{project.name}-#{SecureRandom.hex(8)}"
package.project = project
end
end
@@ -149,23 +155,12 @@ module QA
end
end
- let(:npmrc) do
- {
- file_path: '.npmrc',
- content: <<~NPMRC
- //#{gitlab_host_with_port}/api/v4/projects/#{project.id}/packages/npm/:_authToken=#{auth_token}
- @#{registry_scope}:registry=#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/npm/
- NPMRC
- }
- end
-
- it "push and pull a npm package via CI using a #{params[:token_name]}", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1772' do
+ it "push and pull a npm package via CI using a #{params[:token_name]}" do
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
commit.commit_message = 'Add .gitlab-ci.yml'
commit.add_files([
gitlab_ci_deploy_yaml,
- npmrc,
package_json
])
end
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb
new file mode 100644
index 00000000000..832f8c7f72c
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb
@@ -0,0 +1,197 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Package Registry', :orchestrated, :packages, :reliable, :object_storage do
+ describe 'npm project level endpoint' do
+ using RSpec::Parameterized::TableSyntax
+ include Runtime::Fixtures
+
+ let!(:registry_scope) { Runtime::Namespace.sandbox_name }
+ let!(:personal_access_token) do
+ unless Page::Main::Menu.perform(&:signed_in?)
+ Flow::Login.sign_in
+ end
+
+ Resource::PersonalAccessToken.fabricate!.token
+ end
+
+ let(:project_deploy_token) do
+ Resource::DeployToken.fabricate_via_browser_ui! do |deploy_token|
+ deploy_token.name = 'npm-deploy-token'
+ deploy_token.project = project
+ deploy_token.scopes = [
+ :read_repository,
+ :read_package_registry,
+ :write_package_registry
+ ]
+ end
+ end
+
+ let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) }
+ let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" }
+ let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" }
+
+ let!(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'npm-project-level'
+ end
+ end
+
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = "qa-runner-#{Time.now.to_i}"
+ runner.tags = ["runner-for-#{project.name}"]
+ runner.executor = :docker
+ runner.project = project
+ end
+ end
+
+ let(:gitlab_ci_yaml) do
+ {
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ image: node:latest
+
+ stages:
+ - deploy
+ - install
+
+ deploy:
+ stage: deploy
+ script:
+ - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=#{auth_token}">.npmrc
+ - npm publish
+ only:
+ - "#{project.default_branch}"
+ tags:
+ - "runner-for-#{project.name}"
+ install:
+ stage: install
+ script:
+ - "npm config set @#{registry_scope}:registry #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/"
+ - "npm install #{package.name}"
+ cache:
+ key: ${CI_BUILD_REF_NAME}
+ paths:
+ - node_modules/
+ artifacts:
+ paths:
+ - node_modules/
+ only:
+ - "#{project.default_branch}"
+ tags:
+ - "runner-for-#{project.name}"
+ YAML
+ }
+ end
+
+ let(:package_json) do
+ {
+ file_path: 'package.json',
+ content: <<~JSON
+ {
+ "name": "@#{registry_scope}/mypackage",
+ "version": "1.0.0",
+ "description": "Example package for GitLab npm registry",
+ "publishConfig": {
+ "@#{registry_scope}:registry": "#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/npm/"
+ }
+ }
+ JSON
+ }
+ end
+
+ let(:package) do
+ Resource::Package.init do |package|
+ package.name = "@#{registry_scope}/mypackage-#{SecureRandom.hex(8)}"
+ package.project = project
+ end
+ end
+
+ after do
+ package.remove_via_api!
+ runner.remove_via_api!
+ project.remove_via_api!
+ end
+
+ where(:authentication_token_type, :token_name) do
+ :personal_access_token | 'Personal Access Token'
+ :ci_job_token | 'CI Job Token'
+ :project_deploy_token | 'Deploy Token'
+ end
+
+ with_them do
+ let(:auth_token) do
+ case authentication_token_type
+ when :personal_access_token
+ "\"#{personal_access_token}\""
+ when :ci_job_token
+ '${CI_JOB_TOKEN}'
+ when :project_deploy_token
+ "\"#{project_deploy_token.password}\""
+ end
+ end
+
+ it "push and pull a npm package via CI using a #{params[:token_name]}", quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/344537', type: :investigating } do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([
+ gitlab_ci_yaml,
+ package_json
+ ])
+ end
+
+ project.visit!
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('deploy')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('install')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ job.click_browse_button
+ end
+
+ Page::Project::Artifact::Show.perform do |artifacts|
+ artifacts.go_to_directory('node_modules')
+ artifacts.go_to_directory("@#{registry_scope}")
+ expect(artifacts).to have_content("mypackage")
+ end
+
+ project.visit!
+ Page::Project::Menu.perform(&:click_packages_link)
+
+ Page::Project::Packages::Index.perform do |index|
+ expect(index).to have_package(package.name)
+
+ index.click_package(package.name)
+ end
+
+ Page::Project::Packages::Show.perform do |show|
+ expect(show).to have_package_info(package.name, "1.0.0")
+
+ show.click_delete
+ end
+
+ Page::Project::Packages::Index.perform do |index|
+ expect(index).to have_content("Package deleted successfully")
+ expect(index).not_to have_package(package.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/5_package/nuget_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb
index 8a6752ed817..0b4825715c1 100644
--- a/qa/qa/specs/features/browser_ui/5_package/nuget_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb
@@ -85,7 +85,7 @@ module QA
end
end
- it "publishes a nuget package at the project level, installs and deletes it at the group level using a #{params[:token_name]}", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1073' do
+ it "publishes a nuget package at the project level, installs and deletes it at the group level using a #{params[:token_name]}" do
Flow::Login.sign_in
Resource::Repository::Commit.fabricate_via_api! do |commit|
diff --git a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb
index dfc9202ebed..e727a89a584 100644
--- a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb
@@ -12,7 +12,7 @@ module QA
let(:package) do
Resource::Package.init do |package|
- package.name = 'mypypipackage'
+ package.name = "mypypipackage-#{SecureRandom.hex(8)}"
package.project = project
end
end
@@ -57,7 +57,7 @@ module QA
install:
stage: install
script:
- - "pip install mypypipackage --no-deps --index-url #{uri.scheme}://#{personal_access_token}:#{personal_access_token}@#{gitlab_host_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host #{gitlab_host_with_port}"
+ - "pip install #{package.name} --no-deps --index-url #{uri.scheme}://#{personal_access_token}:#{personal_access_token}@#{gitlab_host_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host #{gitlab_host_with_port}"
tags:
- "runner-for-#{project.name}"
@@ -70,7 +70,7 @@ module QA
import setuptools
setuptools.setup(
- name="mypypipackage",
+ name="#{package.name}",
version="0.0.1",
author="Example Author",
author_email="author@example.com",
diff --git a/qa/qa/specs/features/browser_ui/5_package/rubygems_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
index 9a45b072eed..ecf14a25b8d 100644
--- a/qa/qa/specs/features/browser_ui/5_package/rubygems_registry_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb
@@ -13,7 +13,7 @@ module QA
let(:package) do
Resource::Package.init do |package|
- package.name = 'mygem'
+ package.name = "mygem-#{SecureRandom.hex(8)}"
package.project = project
end
end
@@ -46,17 +46,9 @@ module QA
it 'publishes and deletes a Ruby gem', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1906' do
Flow::Login.sign_in
- Resource::Repository::ProjectPush.fabricate! do |push|
- push.project = project
- push.directory = Pathname
- .new(__dir__)
- .join('../../../../fixtures/rubygems_package')
- push.commit_message = 'RubyGems package'
- end
-
Resource::Repository::Commit.fabricate_via_api! do |commit|
commit.project = project
- commit.commit_message = 'Add mygem.gemspec'
+ commit.commit_message = 'Add package files'
commit.add_files(
[
{
@@ -74,8 +66,8 @@ module QA
echo "#{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/rubygems: '${CI_JOB_TOKEN}'" >> ~/.gem/credentials
- chmod 0600 ~/.gem/credentials
script:
- - gem build mygem
- - gem push mygem-0.0.1.gem --host #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/rubygems
+ - gem build #{package.name}
+ - gem push #{package.name}-0.0.1.gem --host #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/rubygems
tags:
- "runner-for-#{project.name}"
YAML
@@ -90,6 +82,52 @@ module QA
end
end
RUBY
+ },
+ {
+ file_path: "#{package.name}.gemspec",
+ content:
+ <<~RUBY
+ # frozen_string_literal: true
+
+ Gem::Specification.new do |s|
+ s.name = '#{package.name}'
+ s.authors = ['Tanuki Steve', 'Hal 9000']
+ s.author = 'Tanuki Steve'
+ s.version = '0.0.1'
+ s.date = '2011-09-29'
+ s.summary = 'this is a test package'
+ s.files = ['lib/hello_gem.rb']
+ s.require_paths = ['lib']
+
+ s.description = 'A test package for GitLab.'
+ s.email = 'tanuki@not_real.com'
+ s.homepage = 'https://gitlab.com/ruby-co/my-package'
+ s.license = 'MIT'
+
+ s.metadata = {
+ 'bug_tracker_uri' => 'https://gitlab.com/ruby-co/my-package/issues',
+ 'changelog_uri' => 'https://gitlab.com/ruby-co/my-package/CHANGELOG.md',
+ 'documentation_uri' => 'https://gitlab.com/ruby-co/my-package/docs',
+ 'mailing_list_uri' => 'https://gitlab.com/ruby-co/my-package/mailme',
+ 'source_code_uri' => 'https://gitlab.com/ruby-co/my-package'
+ }
+
+ s.bindir = 'bin'
+ s.platform = Gem::Platform::RUBY
+ s.post_install_message = 'Installed, thank you!'
+ s.rdoc_options = ['--main']
+ s.required_ruby_version = '>= 2.7.0'
+ s.required_rubygems_version = '>= 1.8.11'
+ s.requirements = 'A high powered server or calculator'
+ s.rubygems_version = '1.8.09'
+
+ s.add_dependency 'dependency_1', '~> 1.2.3'
+ s.add_dependency 'dependency_2', '3.0.0'
+ s.add_dependency 'dependency_3', '>= 1.0.0'
+ s.add_dependency 'dependency_4'
+ end
+
+ RUBY
}
]
)
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
index 23625ab645d..81ccc585cf9 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb
@@ -12,6 +12,7 @@ module QA
deploy_token = Resource::DeployToken.fabricate_via_browser_ui! do |resource|
resource.name = deploy_token_name
resource.expires_at = one_week_from_now
+ resource.scopes = [:read_repository]
end
expect(deploy_token.username.length).to be > 0
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
index 655c806a37a..e20b76f6bf8 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Configure' do
+ RSpec.describe 'Configure', only: { subdomain: :staging } do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
- project.name = Runtime::Env.auto_devops_project_name || 'autodevops-project'
+ project.name = 'autodevops-project'
project.auto_devops_enabled = true
end
end
@@ -13,35 +13,24 @@ module QA
disable_optional_jobs(project)
end
- describe 'Auto DevOps support', :orchestrated, :kubernetes, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251090', type: :stale } do
+ describe 'Auto DevOps support' do
context 'when rbac is enabled' do
let(:cluster) { Service::KubernetesCluster.new.create! }
after do
cluster&.remove!
+ project.remove_via_api!
end
it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1422' do
Flow::Login.sign_in
- # Set an application secret CI variable (prefixed with K8S_SECRET_)
- Resource::CiVariable.fabricate! do |resource|
- resource.project = project
- resource.key = 'K8S_SECRET_OPTIONAL_MESSAGE'
- resource.value = 'you_can_see_this_variable'
- resource.masked = false
- end
-
- # Connect K8s cluster
Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster|
k8s_cluster.project = project
k8s_cluster.cluster = cluster
k8s_cluster.install_ingress = true
- k8s_cluster.install_prometheus = true
- k8s_cluster.install_runner = true
end
- # Create Auto DevOps compatible repo
Resource::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.directory = Pathname
@@ -78,46 +67,6 @@ module QA
job.click_element(:pipeline_path)
end
-
- Page::Project::Menu.perform(&:go_to_deployments_environments)
- Page::Project::Deployments::Environments::Index.perform do |index|
- index.click_environment_link('production')
- end
- Page::Project::Deployments::Environments::Show.perform do |show|
- show.view_deployment do
- expect(page).to have_content('Hello World!')
- expect(page).to have_content('you_can_see_this_variable')
- end
- end
- end
- end
- end
-
- describe 'Auto DevOps', :smoke do
- before do
- Flow::Login.sign_in
-
- project.visit!
-
- Page::Project::Menu.perform(&:go_to_ci_cd_settings)
- Page::Project::Settings::CiCd.perform(&:expand_auto_devops)
- Page::Project::Settings::AutoDevops.perform(&:enable_autodevops)
-
- # Create AutoDevOps repo
- Resource::Repository::ProjectPush.fabricate! do |push|
- push.project = project
- push.directory = Pathname
- .new(__dir__)
- .join('../../../../../fixtures/auto_devops_rack')
- push.commit_message = 'Create AutoDevOps compatible Project'
- end
- end
-
- it 'runs an AutoDevOps pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1564' do
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- expect(pipeline).to have_tag('Auto DevOps')
end
end
end
@@ -128,7 +77,8 @@ module QA
%w[
CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED
- CONTAINER_SCANNING_DISABLED
+ CONTAINER_SCANNING_DISABLED BROWSER_PERFORMANCE_DISABLED
+ SECRET_DETECTION_DISABLED
].each do |key|
Resource::CiVariable.fabricate_via_api! do |resource|
resource.project = project
diff --git a/qa/qa/specs/helpers/context_selector.rb b/qa/qa/specs/helpers/context_selector.rb
index 57665babf68..9ac79ad6196 100644
--- a/qa/qa/specs/helpers/context_selector.rb
+++ b/qa/qa/specs/helpers/context_selector.rb
@@ -45,11 +45,11 @@ module QA
opts[:subdomain] = case option[:subdomain]
when Array
- "(#{option[:subdomain].join("|")})."
+ "(#{option[:subdomain].join("|")})\\."
when Regexp
option[:subdomain]
else
- "(#{option[:subdomain]})."
+ "(#{option[:subdomain]})\\."
end
end
end
diff --git a/qa/qa/support/fabrication_tracker.rb b/qa/qa/support/fabrication_tracker.rb
new file mode 100644
index 00000000000..3238cc5b0db
--- /dev/null
+++ b/qa/qa/support/fabrication_tracker.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module QA
+ module Support
+ # Threadsafe fabrication time tracker
+ #
+ # Ongoing fabrication is added to callstack by start_fabrication and taken out by finish_fabrication
+ #
+ # Fabrication runtime is saved only for the first fabrication in the stack to properly represent the real time
+ # fabrications might take as top level fabrication runtime will always include nested fabrications runtime
+ #
+ class FabricationTracker
+ class << self
+ # Start fabrication and increment ongoing fabrication count
+ #
+ # @return [void]
+ def start_fabrication
+ Thread.current[:fabrications_ongoing] = 0 unless Thread.current.key?(:fabrications_ongoing)
+
+ Thread.current[:fabrications_ongoing] += 1
+ end
+
+ # Finish fabrication and decrement ongoing fabrication count
+ #
+ # @return [void]
+ def finish_fabrication
+ Thread.current[:fabrications_ongoing] -= 1
+ end
+
+ # Save fabrication time if it's first in fabrication stack
+ #
+ # @param [Symbol] type
+ # @param [Symbol] time
+ # @return [void]
+ def save_fabrication(type, time)
+ return unless Thread.current.key?(type)
+ return unless top_level_fabrication?
+
+ Thread.current[type] += time
+ end
+
+ private
+
+ # Check if current fabrication is the only one in the stack
+ #
+ # @return [Boolean]
+ def top_level_fabrication?
+ Thread.current[:fabrications_ongoing] == 1
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/formatters/test_stats_formatter.rb b/qa/qa/support/formatters/test_stats_formatter.rb
index 0484bd7f90f..b54b6a51d11 100644
--- a/qa/qa/support/formatters/test_stats_formatter.rb
+++ b/qa/qa/support/formatters/test_stats_formatter.rb
@@ -57,6 +57,8 @@ module QA
# @return [Hash]
def test_stats(example)
file_path = example.metadata[:file_path].gsub('./qa/specs/features', '')
+ api_fabrication = ((example.metadata[:api_fabrication] || 0) * 1000).round
+ ui_fabrication = ((example.metadata[:browser_ui_fabrication] || 0) * 1000).round
{
name: 'test-stats',
@@ -76,6 +78,9 @@ module QA
fields: {
id: example.id,
run_time: (example.execution_result.run_time * 1000).round,
+ api_fabrication: api_fabrication,
+ ui_fabrication: ui_fabrication,
+ total_fabrication: api_fabrication + ui_fabrication,
retry_attempts: example.metadata[:retry_attempts] || 0,
job_url: QA::Runtime::Env.ci_job_url,
pipeline_url: env('CI_PIPELINE_URL'),
@@ -98,14 +103,18 @@ module QA
#
# @return [String]
def job_name
- @job_name ||= QA::Runtime::Env.ci_job_name.gsub(%r{ \d{1,2}/\d{1,2}}, '')
+ @job_name ||= QA::Runtime::Env.ci_job_name&.gsub(%r{ \d{1,2}/\d{1,2}}, '')
end
# Single common timestamp for all exported example metrics to keep data points consistently grouped
#
# @return [Time]
def time
- @time ||= DateTime.strptime(env('CI_PIPELINE_CREATED_AT')).to_time
+ @time ||= begin
+ return Time.now unless env('CI_PIPELINE_CREATED_AT')
+
+ DateTime.strptime(env('CI_PIPELINE_CREATED_AT')).to_time
+ end
end
# Is a merge request execution
diff --git a/qa/qa/support/helpers/plan.rb b/qa/qa/support/helpers/plan.rb
new file mode 100644
index 00000000000..298a6d3f036
--- /dev/null
+++ b/qa/qa/support/helpers/plan.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+module QA
+ module Support
+ module Helpers
+ module Plan
+ FREE = { name: 'free', price: 0, yearly_price: 0, ci_minutes: 400 }.freeze
+
+ PREMIUM = {
+ plan_id: '2c92a00d76f0d5060176f2fb0a5029ff',
+ rate_charge_id: '2c92a00d76f0d5060176f2fb0a672a02',
+ name: 'premium',
+ price: 19,
+ yearly_price: 228,
+ ci_minutes: 10000
+ }.freeze
+
+ PREMIUM_SELF_MANAGED = {
+ plan_id: '2c92a01176f0d50a0176f3043c4d4a53',
+ rate_charge_id: '2c92a01176f0d50a0176f3043c6a4a58',
+ name: 'premium',
+ price: 19,
+ yearly_price: 228
+ }.freeze
+
+ ULTIMATE = {
+ plan_id: '2c92a0ff76f0d5250176f2f8c86f305a',
+ rate_charge_id: '2c92a0ff76f0d5250176f2f8c896305c',
+ name: 'ultimate',
+ price: 99,
+ yearly_price: 1188,
+ ci_minutes: 50000
+ }.freeze
+
+ ULTIMATE_SELF_MANAGED = {
+ plan_id: '2c92a00c76f0c6c20176f2f9328b33c9',
+ rate_charge_id: '2c92a00c76f0c6c20176f2fcbb645b5f',
+ name: 'ultimate',
+ price: 99,
+ yearly_price: 1188
+ }.freeze
+
+ CI_MINUTES = {
+ plan_id: '2c92a0086a07f4a8016a2c0a1f7b4b4c',
+ rate_charge_id: '2c92a0fd6a07f4c6016a2c0af07c3f21',
+ name: 'ci_minutes',
+ price: 10,
+ ci_minutes: 1000
+ }.freeze
+
+ STORAGE = {
+ plan_id: '2c92a00f7279a6f5017279d299d01cf9',
+ rate_charge_id: '2c92a0ff7279a74f017279d5bea71fc5',
+ name: 'storage',
+ price: 60,
+ storage: 10
+ }.freeze
+
+ LICENSE_TYPE = {
+ license_file: 'license file',
+ cloud_license: 'cloud license'
+ }.freeze
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/matchers/eventually_matcher.rb b/qa/qa/support/matchers/eventually_matcher.rb
index ff8adab424b..dedef8e6b98 100644
--- a/qa/qa/support/matchers/eventually_matcher.rb
+++ b/qa/qa/support/matchers/eventually_matcher.rb
@@ -59,8 +59,10 @@ module QA
def wait_and_check(actual, expectation_name)
attempt = 0
- QA::Runtime::Logger.debug("Running eventually matcher with '#{operator_msg}' operator")
- QA::Support::Retrier.retry_until(**@retry_args) do
+ QA::Runtime::Logger.debug(
+ "Running eventually matcher with '#{operator_msg}' operator with: #{@retry_args}"
+ )
+ QA::Support::Retrier.retry_until(**@retry_args, log: false) do
QA::Runtime::Logger.debug("evaluating expectation, attempt: #{attempt += 1}")
public_send(expectation_name, actual)
diff --git a/qa/qa/support/matchers/have_matcher.rb b/qa/qa/support/matchers/have_matcher.rb
index 7001f53a7b7..47d2d246460 100644
--- a/qa/qa/support/matchers/have_matcher.rb
+++ b/qa/qa/support/matchers/have_matcher.rb
@@ -19,6 +19,7 @@ module QA
related_issue_item
snippet_description
tag
+ label
].each do |predicate|
RSpec::Matchers.define "have_#{predicate}" do |*args, **kwargs|
match do |page_object|
diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb
index b3a2472d702..a4e8035f964 100644
--- a/qa/qa/support/repeater.rb
+++ b/qa/qa/support/repeater.rb
@@ -18,17 +18,34 @@ module QA
sleep_interval: 0,
raise_on_failure: true,
retry_on_exception: false,
- log: true
+ log: true,
+ message: nil
)
attempts = 0
start = Time.now
begin
while remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration)
- QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if max_attempts && log
+ # start logging from the second attempt
+ if log && attempts == 1
+ msg = ["Retrying action with:"]
+ msg << "max_attempts: #{max_attempts};" if max_attempts
+ msg << "max_duration: #{max_duration};" if max_duration
+ msg << "reload_page: #{reload_page};" if reload_page
+ msg << "sleep_interval: #{sleep_interval};"
+ msg << "raise_on_failure: #{raise_on_failure};"
+ msg << "retry_on_exception: #{retry_on_exception}"
+
+ QA::Runtime::Logger.debug(msg.join(' '))
+ end
+
+ QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if log && max_attempts && attempts > 0
result = yield
- return result if result
+ if result
+ log_completion(log, attempts)
+ return result
+ end
sleep_and_reload_if_needed(sleep_interval, reload_page)
attempts += 1
@@ -47,13 +64,18 @@ module QA
unless remaining_attempts?(attempts, max_attempts)
raise(
RetriesExceededError,
- "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}"
+ "#{message || 'Retry'} failed after #{max_attempts} #{'attempt'.pluralize(max_attempts)}"
)
end
- raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}"
+ raise(
+ WaitExceededError,
+ "#{message || 'Wait'} failed after #{max_duration} #{'second'.pluralize(max_duration)}"
+ )
end
+ log_completion(log, attempts)
+
false
end
@@ -71,6 +93,17 @@ module QA
def remaining_time?(start, max_duration)
max_duration ? Time.now - start < max_duration : true
end
+
+ # Log completion if more than one attempt performed
+ #
+ # @param [Boolean] log
+ # @param [Integer] attempts
+ # @return [void]
+ def log_completion(log, attempts)
+ return unless log && attempts > 0
+
+ QA::Runtime::Logger.debug('ended retry')
+ end
end
end
end
diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb
index aa568d633fc..aa36bf5922e 100644
--- a/qa/qa/support/retrier.rb
+++ b/qa/qa/support/retrier.rb
@@ -7,21 +7,15 @@ module QA
module_function
- def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5, log: true)
- if log
- msg = ["with retry_on_exception: max_attempts: #{max_attempts}"]
- msg << "reload_page: #{reload_page}" if reload_page
- msg << "sleep_interval: #{sleep_interval}"
- QA::Runtime::Logger.debug(msg.join('; '))
- end
-
+ def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5, log: true, message: nil)
result = nil
repeat_until(
max_attempts: max_attempts,
reload_page: reload_page,
sleep_interval: sleep_interval,
retry_on_exception: true,
- log: log
+ log: log,
+ message: message
) do
result = yield
@@ -29,7 +23,6 @@ module QA
# We set it to `true` so that it doesn't repeat if there's no exception
true
end
- QA::Runtime::Logger.debug("ended retry_on_exception") if log
result
end
@@ -41,25 +34,12 @@ module QA
sleep_interval: 0,
raise_on_failure: true,
retry_on_exception: false,
- log: true
+ log: true,
+ message: nil
)
# For backwards-compatibility
max_attempts = 3 if max_attempts.nil? && max_duration.nil?
- if log
- start_msg = ["with retry_until:"]
- start_msg << "max_attempts: #{max_attempts};" if max_attempts
- start_msg << "max_duration: #{max_duration};" if max_duration
- start_msg.push(*[
- "reload_page: #{reload_page};",
- "sleep_interval: #{sleep_interval};",
- "raise_on_failure: #{raise_on_failure};",
- "retry_on_exception: #{retry_on_exception}"
- ])
-
- QA::Runtime::Logger.debug(start_msg.join(' '))
- end
-
result = nil
repeat_until(
max_attempts: max_attempts,
@@ -68,11 +48,11 @@ module QA
sleep_interval: sleep_interval,
raise_on_failure: raise_on_failure,
retry_on_exception: retry_on_exception,
- log: log
+ log: log,
+ message: message
) do
result = yield
end
- QA::Runtime::Logger.debug("ended retry_until") if log
result
end
diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb
index 9ccc0d9484f..6dbbd197b01 100644
--- a/qa/qa/support/waiter.rb
+++ b/qa/qa/support/waiter.rb
@@ -7,20 +7,16 @@ module QA
module_function
- def wait_until(max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME, reload_page: nil, sleep_interval: 0.1, raise_on_failure: true, retry_on_exception: false, log: true)
- if log
- QA::Runtime::Logger.debug(
- <<~MSG.tr("\n", ' ')
- with wait_until: max_duration: #{max_duration};
- reload_page: #{reload_page};
- sleep_interval: #{sleep_interval};
- raise_on_failure: #{raise_on_failure}
- MSG
- )
- end
-
+ def wait_until(
+ max_duration: singleton_class::DEFAULT_MAX_WAIT_TIME,
+ reload_page: nil,
+ sleep_interval: 0.1,
+ raise_on_failure: true,
+ retry_on_exception: false,
+ log: true
+ )
result = nil
- self.repeat_until(
+ repeat_until(
max_duration: max_duration,
reload_page: reload_page,
sleep_interval: sleep_interval,
@@ -30,7 +26,6 @@ module QA
) do
result = yield
end
- QA::Runtime::Logger.debug("ended wait_until") if log
result
end
diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb
new file mode 100644
index 00000000000..9d2079171c1
--- /dev/null
+++ b/qa/qa/tools/reliable_report.rb
@@ -0,0 +1,234 @@
+# frozen_string_literal: true
+
+require "influxdb-client"
+require "terminal-table"
+require "slack-notifier"
+
+module QA
+ module Tools
+ class ReliableReport
+ def initialize(run_type, range = 30)
+ @results = 10
+ @slack_channel = "#quality-reports"
+ @range = range
+ @run_type = run_type
+ @stable_title = "Top #{results} stable specs for past #{@range} days in '#{run_type}' runs"
+ @unstable_title = "Top #{results} unstable reliable specs for past #{@range} days in '#{run_type}' runs"
+ end
+
+ # Print top stable specs
+ #
+ # @return [void]
+ def show_top_stable
+ puts terminal_table(
+ rows: top_stable.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] },
+ title: stable_title
+ )
+ end
+
+ # Post top stable spec report to slack
+ # Slice table in to multiple messages due to max char limitation
+ #
+ # @return [void]
+ def notify_top_stable
+ tables = top_stable.each_slice(5).map do |slice|
+ terminal_table(
+ rows: slice.map { |spec| [name_column(spec[0], spec[1][:file]), *table_params(spec[1].values)] }
+ )
+ end
+
+ puts "\nSending top stable spec report to #{slack_channel} slack channel"
+ slack_args = { icon_emoji: ":mtg_green:", username: "Stable Spec Report" }
+ notifier.post(text: "*#{stable_title}*", **slack_args)
+ tables.each { |table| notifier.post(text: "```#{table}```", **slack_args) }
+ end
+
+ # Print top unstable specs
+ #
+ # @return [void]
+ def show_top_unstable
+ return puts("No unstable tests present!") if top_unstable_reliable.empty?
+
+ puts terminal_table(
+ rows: top_unstable_reliable.map { |k, v| [name_column(k, v[:file]), *table_params(v.values)] },
+ title: unstable_title
+ )
+ end
+
+ # Post top unstable reliable spec report to slack
+ # Slice table in to multiple messages due to max char limitation
+ #
+ # @return [void]
+ def notify_top_unstable
+ return puts("No unstable tests present!") if top_unstable_reliable.empty?
+
+ tables = top_unstable_reliable.each_slice(5).map do |slice|
+ terminal_table(
+ rows: slice.map { |spec| [name_column(spec[0], spec[1][:file]), *table_params(spec[1].values)] }
+ )
+ end
+
+ puts "\nSending top unstable reliable spec report to #{slack_channel} slack channel"
+ slack_args = { icon_emoji: ":sadpanda:", username: "Unstable Spec Report" }
+ notifier.post(text: "*#{unstable_title}*", **slack_args)
+ tables.each { |table| notifier.post(text: "```#{table}```", **slack_args) }
+ end
+
+ private
+
+ attr_reader :results,
+ :slack_channel,
+ :range,
+ :run_type,
+ :stable_title,
+ :unstable_title
+
+ # Top stable specs
+ #
+ # @return [Hash]
+ def top_stable
+ @top_stable ||= runs(reliable: false).sort_by { |k, v| [v[:failure_rate], -v[:runs]] }[0..results - 1].to_h
+ end
+
+ # Top unstable reliable specs
+ #
+ # @return [Hash]
+ def top_unstable_reliable
+ @top_unstable_reliable ||= runs(reliable: true)
+ .reject { |k, v| v[:failure_rate] == 0 }
+ .sort_by { |k, v| -v[:failure_rate] }[0..results - 1]
+ .to_h
+ end
+
+ # Terminal table for result formatting
+ #
+ # @return [Terminal::Table]
+ def terminal_table(rows:, title: nil)
+ Terminal::Table.new(
+ headings: ["name", "runs", "failed", "failure rate"],
+ style: { all_separators: true },
+ title: title,
+ rows: rows
+ )
+ end
+
+ # Spec parameters for table row
+ #
+ # @param [Array] parameters
+ # @return [Array]
+ def table_params(parameters)
+ [*parameters[1..2], "#{parameters.last}%"]
+ end
+
+ # Name column value
+ #
+ # @param [String] name
+ # @param [String] file
+ # @return [String]
+ def name_column(name, file)
+ spec_name = name.length > 100 ? "#{name} ".scan(/.{1,100} /).map(&:strip).join("\n") : name
+ name_line = "name: '#{spec_name}'"
+ file_line = "file: '#{file}'"
+
+ "#{name_line}\n#{file_line.ljust(110)}"
+ end
+
+ # Test executions grouped by name
+ #
+ # @param [Boolean] reliable
+ # @return [Hash]
+ def runs(reliable:)
+ puts("Fetching data on #{reliable ? 'reliable ' : ''}test execution for past 30 days in '#{run_type}' runs")
+ puts
+
+ query_api.query(query: query(reliable)).values.each_with_object({}) do |table, result|
+ records = table.records
+ name = records.last.values["name"]
+ file = records.last.values["file_path"].split("/").last
+ runs = records.count
+ failed = records.count { |r| r.values["status"] == "failed" }
+ failure_rate = (failed.to_f / runs.to_f) * 100
+
+ result[name] = {
+ file: file,
+ runs: runs,
+ failed: failed,
+ failure_rate: failure_rate == 0 ? failure_rate.round(0) : failure_rate.round(2)
+ }
+ end
+ end
+
+ # Flux query
+ #
+ # @param [Boolean] reliable
+ # @return [String]
+ def query(reliable)
+ <<~QUERY
+ from(bucket: "e2e-test-stats")
+ |> range(start: -#{range}d)
+ |> filter(fn: (r) => r._measurement == "test-stats" and
+ r.run_type == "#{run_type}" and
+ r.status != "pending" and
+ r.merge_request == "false" and
+ r.quarantined == "false" and
+ r.reliable == "#{reliable}" and
+ r._field == "id"
+ )
+ |> group(columns: ["name"])
+ QUERY
+ end
+
+ # Query client
+ #
+ # @return [QueryApi]
+ def query_api
+ @query_api ||= influx_client.create_query_api
+ end
+
+ # InfluxDb client
+ #
+ # @return [InfluxDB2::Client]
+ def influx_client
+ @influx_client ||= InfluxDB2::Client.new(
+ influxdb_url,
+ influxdb_token,
+ bucket: "e2e-test-stats",
+ org: "gitlab-qa",
+ precision: InfluxDB2::WritePrecision::NANOSECOND
+ )
+ end
+
+ # Slack notifier
+ #
+ # @return [Slack::Notifier]
+ def notifier
+ @notifier ||= Slack::Notifier.new(
+ slack_webhook_url,
+ channel: slack_channel,
+ username: "Reliable spec reporter"
+ )
+ end
+
+ # InfluxDb instance url
+ #
+ # @return [String]
+ def influxdb_url
+ @influxdb_url ||= ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL environment variable")
+ end
+
+ # Influxdb token
+ #
+ # @return [String]
+ def influxdb_token
+ @influxdb_token ||= ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN environment variable")
+ end
+
+ # Slack webhook url
+ #
+ # @return [String]
+ def slack_webhook_url
+ @slack_webhook_url ||= ENV["CI_SLACK_WEBHOOK_URL"] || raise("Missing CI_SLACK_WEBHOOK_URL environment variable")
+ end
+ end
+ end
+end
diff --git a/qa/spec/runtime/feature_spec.rb b/qa/spec/runtime/feature_spec.rb
index 39c20dd3070..88f5cd5be93 100644
--- a/qa/spec/runtime/feature_spec.rb
+++ b/qa/spec/runtime/feature_spec.rb
@@ -175,6 +175,20 @@ RSpec.describe QA::Runtime::Feature do
expect(described_class.enabled?(feature_flag)).to be_truthy
end
+ it 'raises an error when the scope is unknown' do
+ expect(QA::Runtime::API::Request)
+ .to receive(:new)
+ .with(api_client, "/features")
+ .and_return(request)
+ expect(described_class)
+ .to receive(:get)
+ .and_return(
+ Struct.new(:code, :body)
+ .new(200, %([{ "name": "a_flag", "state": "conditional", "gates": { "key": "groups", "value": ["foo"] } }])))
+
+ expect { described_class.enabled?(feature_flag, scope: 'foo') }.to raise_error(QA::Runtime::Feature::UnknownScopeError)
+ end
+
context 'when a project scope is provided' do
it_behaves_like 'checks a feature flag' do
let(:scope) { :project }
@@ -212,4 +226,38 @@ RSpec.describe QA::Runtime::Feature do
end
end
end
+
+ describe '.set' do
+ let(:scope) { { scope: 'actor' } }
+
+ it 'raises an error when the flag state is unknown' do
+ expect(described_class).not_to receive(:enable)
+ expect(described_class).not_to receive(:disable)
+
+ expect { described_class.set({ foo: 'bar' }, **scope) }.to raise_error(QA::Runtime::Feature::UnknownStateError, 'Unknown feature flag state: bar')
+ end
+
+ it 'enables feature flags' do
+ expect(described_class).to receive(:enable).with(:flag1, scope)
+ expect(described_class).to receive(:enable).with(:flag2, scope)
+ expect(described_class).not_to receive(:disable)
+
+ described_class.set({ flag1: 'enabled', flag2: 'enable' }, **scope)
+ end
+
+ it 'disables feature flags' do
+ expect(described_class).to receive(:disable).with(:flag1, scope)
+ expect(described_class).to receive(:disable).with(:flag2, scope)
+ expect(described_class).not_to receive(:enable)
+
+ described_class.set({ flag1: 'disable', flag2: 'disable' }, **scope)
+ end
+
+ it 'enables and disables feature flags' do
+ expect(described_class).to receive(:enable).with(:flag1, scope)
+ expect(described_class).to receive(:disable).with(:flag2, scope)
+
+ described_class.set({ flag1: 'enabled', flag2: 'disabled' }, **scope)
+ end
+ end
end
diff --git a/qa/spec/scenario/test/instance/reliable_spec.rb b/qa/spec/scenario/test/instance/reliable_spec.rb
new file mode 100644
index 00000000000..4001d386bf3
--- /dev/null
+++ b/qa/spec/scenario/test/instance/reliable_spec.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+RSpec.describe QA::Scenario::Test::Instance::Reliable do
+ it_behaves_like 'a QA scenario class' do
+ let(:tags) { [:reliable] }
+ end
+end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index e25892a008f..640f2de0ca2 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -27,8 +27,12 @@ RSpec.configure do |config|
config.add_formatter QA::Support::Formatters::QuarantineFormatter
config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics?
- config.before do |example|
+ config.prepend_before do |example|
QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n")
+
+ # Reset fabrication counters tracked in resource base
+ Thread.current[:api_fabrication] = 0
+ Thread.current[:browser_ui_fabrication] = 0
end
config.after do
@@ -36,6 +40,12 @@ RSpec.configure do |config|
QA::Git::Repository.new.delete_netrc
end
+ # Add fabrication time to spec metadata
+ config.append_after do |example|
+ example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
+ example.metadata[:browser_ui_fabrication] = Thread.current[:browser_ui_fabrication]
+ end
+
config.after(:context) do
if !QA::Runtime::Browser.blank_page? && QA::Page::Main::Menu.perform(&:signed_in?)
QA::Page::Main::Menu.perform(&:sign_out)
diff --git a/qa/spec/specs/allure_report_spec.rb b/qa/spec/specs/allure_report_spec.rb
index 03bf77039cc..06b09106140 100644
--- a/qa/spec/specs/allure_report_spec.rb
+++ b/qa/spec/specs/allure_report_spec.rb
@@ -3,7 +3,7 @@
describe QA::Runtime::AllureReport do
include QA::Support::Helpers::StubEnv
- let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, after: nil) }
+ let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, append_after: nil) }
let(:png_path) { 'png_path' }
let(:html_path) { 'html_path' }
@@ -46,6 +46,8 @@ describe QA::Runtime::AllureReport do
let(:html_file) { 'html-file' }
let(:ci_job) { 'ee:relative 5' }
let(:versions) { { version: '14', revision: '6ced31db947' } }
+ let(:session) { double('session') }
+ let(:browser_log) { ['log message 1', 'log message 2'] }
before do
stub_env('CI', 'true')
@@ -58,6 +60,9 @@ describe QA::Runtime::AllureReport do
allow(RestClient::Request).to receive(:execute) { double('response', code: 200, body: versions.to_json) }
allow(QA::Runtime::Scenario).to receive(:method_missing).with(:gitlab_address).and_return('gitlab.com')
+ allow(Capybara).to receive(:current_session).and_return(session)
+ allow(session).to receive_message_chain('driver.browser.logs.get').and_return(browser_log)
+
described_class.configure!
end
@@ -76,7 +81,11 @@ describe QA::Runtime::AllureReport do
.with(QA::Support::Formatters::AllureMetadataFormatter).ordered
end
- it 'configures screenshot saving' do
+ it 'configures attachments saving' do
+ expect(rspec_config).to have_received(:append_after) do |&arg|
+ arg.call
+ end
+
aggregate_failures do
expect(Allure).to have_received(:add_attachment).with(
name: 'screenshot',
@@ -90,6 +99,12 @@ describe QA::Runtime::AllureReport do
type: 'text/html',
test_case: true
)
+ expect(Allure).to have_received(:add_attachment).with(
+ name: 'browser.log',
+ source: browser_log.join("\n\n"),
+ type: Allure::ContentType::TXT,
+ test_case: true
+ )
end
end
end
diff --git a/qa/spec/specs/helpers/context_selector_spec.rb b/qa/spec/specs/helpers/context_selector_spec.rb
index 0152fee6f5b..5a320cde71f 100644
--- a/qa/spec/specs/helpers/context_selector_spec.rb
+++ b/qa/spec/specs/helpers/context_selector_spec.rb
@@ -186,6 +186,24 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do
end
end
+ context 'staging-ref' do
+ before do
+ QA::Runtime::Scenario.define(:gitlab_address, 'https://staging-ref.gitlab.com/')
+ end
+
+ it 'runs on staging-ref' do
+ group = describe_successfully do
+ it('does not run in staging', only: { subdomain: :staging }) {}
+ it('runs in staging-ref', only: { subdomain: /^staging-ref./ }) {}
+ end
+
+ aggregate_failures do
+ expect(group.examples[0].execution_result.status).to eq(:pending)
+ expect(group.examples[1].execution_result.status).to eq(:passed)
+ end
+ end
+ end
+
context 'production' do
before do
QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com/')
diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_stats_formatter_spec.rb
index 859d45a660b..f9baf9bd9d9 100644
--- a/qa/spec/support/formatters/test_stats_formatter_spec.rb
+++ b/qa/spec/support/formatters/test_stats_formatter_spec.rb
@@ -20,6 +20,8 @@ describe QA::Support::Formatters::TestStatsFormatter do
let(:influx_write_api) { instance_double('InfluxDB2::WriteApi', write: nil) }
let(:stage) { '1_manage' }
let(:file_path) { "./qa/specs/features/#{stage}/subfolder/some_spec.rb" }
+ let(:ui_fabrication) { 0 }
+ let(:api_fabrication) { 0 }
let(:influx_client_args) do
{
@@ -48,6 +50,9 @@ describe QA::Support::Formatters::TestStatsFormatter do
fields: {
id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]',
run_time: 0,
+ api_fabrication: api_fabrication * 1000,
+ ui_fabrication: ui_fabrication * 1000,
+ total_fabrication: (api_fabrication + ui_fabrication) * 1000,
retry_attempts: 0,
job_url: ci_job_url,
pipeline_url: ci_pipeline_url,
@@ -69,6 +74,11 @@ describe QA::Support::Formatters::TestStatsFormatter do
RSpec::Core::Sandbox.sandboxed do |config|
config.formatter = QA::Support::Formatters::TestStatsFormatter
+ config.append_after do |example|
+ example.metadata[:api_fabrication] = Thread.current[:api_fabrication]
+ example.metadata[:browser_ui_fabrication] = Thread.current[:browser_ui_fabrication]
+ end
+
config.before(:context) { RSpec.current_example = nil }
example.run
@@ -171,5 +181,21 @@ describe QA::Support::Formatters::TestStatsFormatter do
expect(influx_write_api).to have_received(:write).with(data: [data])
end
end
+
+ context 'with fabrication runtimes' do
+ let(:ui_fabrication) { 10 }
+ let(:api_fabrication) { 4 }
+
+ before do
+ Thread.current[:api_fabrication] = api_fabrication
+ Thread.current[:browser_ui_fabrication] = ui_fabrication
+ end
+
+ it 'exports data to influxdb with fabrication times' do
+ run_spec
+
+ expect(influx_write_api).to have_received(:write).with(data: [data])
+ end
+ end
end
end
diff --git a/qa/spec/support/repeater_spec.rb b/qa/spec/support/repeater_spec.rb
index da8d6b18fb0..4fa3bcde5e7 100644
--- a/qa/spec/support/repeater_spec.rb
+++ b/qa/spec/support/repeater_spec.rb
@@ -23,7 +23,7 @@ RSpec.describe QA::Support::Repeater do
context 'when retry_on_exception is not provided (default: false)' do
context 'when max_duration is provided' do
context 'when max duration is reached' do
- it 'raises an exception' do
+ it 'raises an exception with default message' do
expect do
Timecop.freeze do
subject.repeat_until(max_duration: 1) do
@@ -31,7 +31,20 @@ RSpec.describe QA::Support::Repeater do
false
end
end
- end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second")
+ end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait failed after 1 second")
+ end
+
+ it 'raises an exception with custom message' do
+ message = 'Some custom action'
+
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_duration: 1, message: message) do
+ Timecop.travel(2)
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::WaitExceededError, "#{message} failed after 1 second")
end
it 'ignores attempts' do
@@ -70,14 +83,26 @@ RSpec.describe QA::Support::Repeater do
context 'when max_attempts is provided' do
context 'when max_attempts is reached' do
- it 'raises an exception' do
+ it 'raises an exception with default message' do
expect do
Timecop.freeze do
subject.repeat_until(max_attempts: 1) do
false
end
end
- end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt")
+ end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry failed after 1 attempt")
+ end
+
+ it 'raises an exception with custom message' do
+ message = 'Some custom action'
+
+ expect do
+ Timecop.freeze do
+ subject.repeat_until(max_attempts: 1, message: message) do
+ false
+ end
+ end
+ end.to raise_error(QA::Support::Repeater::RetriesExceededError, "#{message} failed after 1 attempt")
end
it 'ignores duration' do
@@ -126,7 +151,7 @@ RSpec.describe QA::Support::Repeater do
false
end
end
- end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt")
+ end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry failed after 1 attempt")
end
end
@@ -141,7 +166,7 @@ RSpec.describe QA::Support::Repeater do
false
end
end
- end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second")
+ end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait failed after 1 second")
end
end
end
@@ -210,7 +235,7 @@ RSpec.describe QA::Support::Repeater do
false
end
end
- end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry condition not met after 1 attempt")
+ end.to raise_error(QA::Support::Repeater::RetriesExceededError, "Retry failed after 1 attempt")
end
end
@@ -225,7 +250,7 @@ RSpec.describe QA::Support::Repeater do
false
end
end
- end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait condition not met after 1 second")
+ end.to raise_error(QA::Support::Repeater::WaitExceededError, "Wait failed after 1 second")
end
end
end
@@ -380,34 +405,67 @@ RSpec.describe QA::Support::Repeater do
end
end
- it 'logs attempts' do
- attempted = false
+ context 'with logging' do
+ before do
+ allow(QA::Runtime::Logger).to receive(:debug)
+ end
- expect do
- subject.repeat_until(max_attempts: 1) do
- unless attempted
- attempted = true
- break false
- end
+ it 'skips logging single attempt with max_attempts' do
+ subject.repeat_until(max_attempts: 3) do
+ true
+ end
+ expect(QA::Runtime::Logger).not_to have_received(:debug)
+ end
+
+ it 'skips logging single attempt with max_duration' do
+ subject.repeat_until(max_duration: 3) do
true
end
- end.to output(/Attempt number/).to_stdout_from_any_process
- end
- it 'allows logging to be silenced' do
- attempted = false
+ expect(QA::Runtime::Logger).not_to have_received(:debug)
+ end
- expect do
- subject.repeat_until(max_attempts: 1, log: false) do
- unless attempted
- attempted = true
- break false
- end
+ it 'allows logging to be silenced' do
+ subject.repeat_until(max_attempts: 3, log: false, raise_on_failure: false) do
+ false
+ end
- true
+ expect(QA::Runtime::Logger).not_to have_received(:debug)
+ end
+
+ it 'starts logging on subsequent attempts for max_duration' do
+ subject.repeat_until(max_duration: 0.3, sleep_interval: 0.1, raise_on_failure: false) do
+ false
+ end
+
+ aggregate_failures do
+ expect(QA::Runtime::Logger).to have_received(:debug).with(<<~MSG.strip).ordered.once
+ Retrying action with: max_duration: 0.3; sleep_interval: 0.1; raise_on_failure: false; retry_on_exception: false
+ MSG
+ expect(QA::Runtime::Logger).to have_received(:debug).with('ended retry').ordered.once
+ expect(QA::Runtime::Logger).not_to have_received(:debug).with(/Attempt number/)
+ end
+ end
+
+ it 'starts logging subsequent attempts for max_attempts' do
+ attempts = 0
+ subject.repeat_until(max_attempts: 4, raise_on_failure: false) do
+ next true if attempts == 2
+
+ attempts += 1
+ false
end
- end.not_to output.to_stdout_from_any_process
+
+ aggregate_failures do
+ expect(QA::Runtime::Logger).to have_received(:debug).with(<<~MSG.strip).ordered.once
+ Retrying action with: max_attempts: 4; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false
+ MSG
+ expect(QA::Runtime::Logger).to have_received(:debug).with('Attempt number 2').ordered.once
+ expect(QA::Runtime::Logger).to have_received(:debug).with('Attempt number 3').ordered.once
+ expect(QA::Runtime::Logger).to have_received(:debug).with('ended retry').ordered.once
+ end
+ end
end
end
end
diff --git a/qa/spec/support/retrier_spec.rb b/qa/spec/support/retrier_spec.rb
index 9ad3e85fea9..1f303093a00 100644
--- a/qa/spec/support/retrier_spec.rb
+++ b/qa/spec/support/retrier_spec.rb
@@ -1,42 +1,7 @@
# frozen_string_literal: true
RSpec.describe QA::Support::Retrier do
- before do
- logger = ::Logger.new $stdout
- logger.level = ::Logger::DEBUG
- QA::Runtime::Logger.logger = logger
- end
-
describe '.retry_until' do
- context 'when the condition is true' do
- it 'logs max attempts (3 by default)' do
- expect { subject.retry_until { true } }
- .to output(/with retry_until: max_attempts: 3; reload_page: ; sleep_interval: 0; raise_on_failure: true; retry_on_exception: false/).to_stdout_from_any_process
- end
-
- it 'logs max duration' do
- expect { subject.retry_until(max_duration: 1) { true } }
- .to output(/with retry_until: max_duration: 1; reload_page: ; sleep_interval: 0; raise_on_failure: true; retry_on_exception: false/).to_stdout_from_any_process
- end
-
- it 'logs the end' do
- expect { subject.retry_until { true } }
- .to output(/ended retry_until$/).to_stdout_from_any_process
- end
- end
-
- context 'when the condition is false' do
- it 'logs the start' do
- expect { subject.retry_until(max_duration: 0, raise_on_failure: false) { false } }
- .to output(/with retry_until: max_duration: 0; reload_page: ; sleep_interval: 0; raise_on_failure: false; retry_on_exception: false/).to_stdout_from_any_process
- end
-
- it 'logs the end' do
- expect { subject.retry_until(max_duration: 0, raise_on_failure: false) { false } }
- .to output(/ended retry_until$/).to_stdout_from_any_process
- end
- end
-
context 'when max_duration and max_attempts are nil' do
it 'sets max attempts to 3 by default' do
expect(subject).to receive(:repeat_until).with(hash_including(max_attempts: 3))
@@ -62,35 +27,21 @@ RSpec.describe QA::Support::Retrier do
subject.retry_until
end
- end
- describe '.retry_on_exception' do
- context 'when the condition is true' do
- it 'logs max_attempts, reload_page, and sleep_interval parameters' do
- message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
- expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { true } }
- .to output(message).to_stdout_from_any_process
- end
+ it 'allows logs to be silenced' do
+ expect(subject).to receive(:repeat_until).with(hash_including(log: false))
- it 'logs the end' do
- expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } }
- .to output(/ended retry_on_exception$/).to_stdout_from_any_process
- end
+ subject.retry_until(log: false)
end
- context 'when the condition is false' do
- it 'logs the start' do
- message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/
- expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { false } }
- .to output(message).to_stdout_from_any_process
- end
+ it 'sets custom error message' do
+ expect(subject).to receive(:repeat_until).with(hash_including(message: 'Custom message'))
- it 'logs the end' do
- expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } }
- .to output(/ended retry_on_exception$/).to_stdout_from_any_process
- end
+ subject.retry_until(message: 'Custom message')
end
+ end
+ describe '.retry_on_exception' do
it 'does not repeat if no exception is raised' do
loop_counter = 0
return_value = "test passed"
@@ -121,5 +72,11 @@ RSpec.describe QA::Support::Retrier do
subject.retry_on_exception
end
+
+ it 'allows logs to be silenced' do
+ expect(subject).to receive(:repeat_until).with(hash_including(log: false))
+
+ subject.retry_on_exception(log: false)
+ end
end
end
diff --git a/qa/spec/support/shared_contexts/packages_registry_shared_context.rb b/qa/spec/support/shared_contexts/packages_registry_shared_context.rb
index 6e197015640..e686d254a44 100644
--- a/qa/spec/support/shared_contexts/packages_registry_shared_context.rb
+++ b/qa/spec/support/shared_contexts/packages_registry_shared_context.rb
@@ -43,8 +43,13 @@ module QA
let(:project_deploy_token) do
Resource::DeployToken.fabricate_via_browser_ui! do |deploy_token|
- deploy_token.name = 'helm-package-deploy-token'
+ deploy_token.name = 'package-deploy-token'
deploy_token.project = package_project
+ deploy_token.scopes = [
+ :read_repository,
+ :read_package_registry,
+ :write_package_registry
+ ]
end
end
diff --git a/qa/spec/support/waiter_spec.rb b/qa/spec/support/waiter_spec.rb
index d0b216b5dc1..c575a27bc35 100644
--- a/qa/spec/support/waiter_spec.rb
+++ b/qa/spec/support/waiter_spec.rb
@@ -1,40 +1,11 @@
# frozen_string_literal: true
RSpec.describe QA::Support::Waiter do
- before do
- logger = ::Logger.new $stdout
- logger.level = ::Logger::DEBUG
- QA::Runtime::Logger.logger = logger
- end
-
describe '.wait_until' do
- context 'when the condition is true' do
- it 'logs the start' do
- expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { true } }
- .to output(/with wait_until: max_duration: 0; reload_page: ; sleep_interval: 0.1/).to_stdout_from_any_process
- end
-
- it 'logs the end' do
- expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { true } }
- .to output(/ended wait_until$/).to_stdout_from_any_process
- end
- end
-
- context 'when the condition is false' do
- it 'logs the start' do
- expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { false } }
- .to output(/with wait_until: max_duration: 0; reload_page: ; sleep_interval: 0.1/).to_stdout_from_any_process
- end
-
- it 'logs the end' do
- expect { subject.wait_until(max_duration: 0, raise_on_failure: false) { false } }
- .to output(/ended wait_until$/).to_stdout_from_any_process
- end
- end
-
it 'allows logs to be silenced' do
- expect { subject.wait_until(max_duration: 0, raise_on_failure: false, log: false) { false } }
- .not_to output.to_stdout_from_any_process
+ expect(subject).to receive(:repeat_until).with(hash_including(log: false))
+
+ subject.wait_until(log: false)
end
it 'sets max_duration to 60 by default' do
diff --git a/qa/spec/tools/reliable_report_spec.rb b/qa/spec/tools/reliable_report_spec.rb
new file mode 100644
index 00000000000..c7d4d28fb21
--- /dev/null
+++ b/qa/spec/tools/reliable_report_spec.rb
@@ -0,0 +1,145 @@
+# frozen_string_literal: true
+
+describe QA::Tools::ReliableReport do
+ include QA::Support::Helpers::StubEnv
+
+ subject(:reporter) { described_class.new(run_type, range) }
+
+ let(:slack_notifier) { instance_double("Slack::Notifier", post: nil) }
+ let(:influx_client) { instance_double("InfluxDB2::Client", create_query_api: query_api) }
+ let(:query_api) { instance_double("InfluxDB2::QueryApi") }
+
+ let(:slack_channel) { "#quality-reports" }
+ let(:run_type) { "package-and-qa" }
+ let(:range) { 30 }
+ let(:results) { 10 }
+
+ let(:runs) { { 0 => stable_spec, 1 => unstable_spec } }
+
+ let(:stable_spec) do
+ spec_values = { "name" => "stable spec", "status" => "passed", "file_path" => "some/spec.rb" }
+ instance_double(
+ "InfluxDB2::FluxTable",
+ records: [
+ instance_double("InfluxDB2::FluxRecord", values: spec_values),
+ instance_double("InfluxDB2::FluxRecord", values: spec_values),
+ instance_double("InfluxDB2::FluxRecord", values: spec_values)
+ ]
+ )
+ end
+
+ let(:unstable_spec) do
+ spec_values = { "name" => "unstable spec", "status" => "failed", "file_path" => "some/spec.rb" }
+ instance_double(
+ "InfluxDB2::FluxTable",
+ records: [
+ instance_double("InfluxDB2::FluxRecord", values: { **spec_values, "status" => "passed" }),
+ instance_double("InfluxDB2::FluxRecord", values: spec_values),
+ instance_double("InfluxDB2::FluxRecord", values: spec_values)
+ ]
+ )
+ end
+
+ def flux_query(reliable)
+ <<~QUERY
+ from(bucket: "e2e-test-stats")
+ |> range(start: -#{range}d)
+ |> filter(fn: (r) => r._measurement == "test-stats" and
+ r.run_type == "#{run_type}" and
+ r.status != "pending" and
+ r.merge_request == "false" and
+ r.quarantined == "false" and
+ r.reliable == "#{reliable}" and
+ r._field == "id"
+ )
+ |> group(columns: ["name"])
+ QUERY
+ end
+
+ def table(rows, title = nil)
+ Terminal::Table.new(
+ headings: ["name", "runs", "failed", "failure rate"],
+ style: { all_separators: true },
+ title: title,
+ rows: rows
+ )
+ end
+
+ def name_column(spec_name)
+ name = "name: '#{spec_name}'"
+ file = "file: 'spec.rb'".ljust(110)
+
+ "#{name}\n#{file}"
+ end
+
+ before do
+ stub_env("QA_INFLUXDB_URL", "url")
+ stub_env("QA_INFLUXDB_TOKEN", "token")
+ stub_env("CI_SLACK_WEBHOOK_URL", "slack_url")
+
+ allow(Slack::Notifier).to receive(:new).and_return(slack_notifier)
+ allow(InfluxDB2::Client).to receive(:new).and_return(influx_client)
+ allow(query_api).to receive(:query).with(query: query).and_return(runs)
+ end
+
+ context "with stable spec report" do
+ let(:query) { flux_query(false) }
+ let(:fetch_message) { "Fetching data on test execution for past #{range} days in '#{run_type}' runs" }
+ let(:slack_send_message) { "Sending top stable spec report to #{slack_channel} slack channel" }
+ let(:title) { "Top #{results} stable specs for past #{range} days in '#{run_type}' runs" }
+ let(:rows) do
+ [
+ [name_column("stable spec"), 3, 0, "0%"],
+ [name_column("unstable spec"), 3, 2, "66.67%"]
+ ]
+ end
+
+ it "prints top stable spec report to console" do
+ expect { reporter.show_top_stable }.to output("#{fetch_message}\n\n#{table(rows, title)}\n").to_stdout
+ end
+
+ it "sends top stable spec report to slack" do
+ slack_args = { icon_emoji: ":mtg_green:", username: "Stable Spec Report" }
+
+ expect { reporter.notify_top_stable }.to output("#{fetch_message}\n\n\n#{slack_send_message}\n").to_stdout
+ expect(slack_notifier).to have_received(:post).with(text: "*#{title}*", **slack_args)
+ expect(slack_notifier).to have_received(:post).with(text: "```#{table(rows)}```", **slack_args)
+ end
+ end
+
+ context "with unstable spec report" do
+ let(:query) { flux_query(true) }
+ let(:fetch_message) { "Fetching data on reliable test execution for past #{range} days in '#{run_type}' runs" }
+ let(:slack_send_message) { "Sending top unstable reliable spec report to #{slack_channel} slack channel" }
+ let(:title) { "Top #{results} unstable reliable specs for past #{range} days in '#{run_type}' runs" }
+ let(:rows) { [[name_column("unstable spec"), 3, 2, "66.67%"]] }
+
+ it "prints top unstable spec report to console" do
+ expect { reporter.show_top_unstable }.to output("#{fetch_message}\n\n#{table(rows, title)}\n").to_stdout
+ end
+
+ it "sends top unstable reliable spec report to slack" do
+ slack_args = { icon_emoji: ":sadpanda:", username: "Unstable Spec Report" }
+
+ expect { reporter.notify_top_unstable }.to output("#{fetch_message}\n\n\n#{slack_send_message}\n").to_stdout
+ expect(slack_notifier).to have_received(:post).with(text: "*#{title}*", **slack_args)
+ expect(slack_notifier).to have_received(:post).with(text: "```#{table(rows)}```", **slack_args)
+ end
+ end
+
+ context "without unstable reliable specs" do
+ let(:query) { flux_query(true) }
+ let(:runs) { { 0 => stable_spec } }
+ let(:fetch_message) { "Fetching data on reliable test execution for past #{range} days in '#{run_type}' runs" }
+ let(:no_result_message) { "No unstable tests present!" }
+
+ it "prints no result message to console" do
+ expect { reporter.show_top_unstable }.to output("#{fetch_message}\n\n#{no_result_message}\n").to_stdout
+ end
+
+ it "skips slack notification" do
+ expect { reporter.notify_top_unstable }.to output("#{fetch_message}\n\n#{no_result_message}\n").to_stdout
+ expect(slack_notifier).not_to have_received(:post)
+ end
+ end
+end
diff --git a/qa/tasks/reliable_report.rake b/qa/tasks/reliable_report.rake
new file mode 100644
index 00000000000..204c959093a
--- /dev/null
+++ b/qa/tasks/reliable_report.rake
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+# rubocop:disable Rails/RakeEnvironment
+
+require_relative "../qa/tools/reliable_report"
+
+desc "Fetch top most reliable specs"
+task :reliable_spec_report, [:run_type, :range, :create_slack_report] do |_task, args|
+ report = QA::Tools::ReliableReport.new(args[:run_type] || "package-and-qa", args[:range])
+
+ report.show_top_stable
+ report.notify_top_stable if args[:create_slack_report] == 'true'
+end
+
+desc "Fetch top most unstable reliable specs"
+task :unreliable_spec_report, [:run_type, :range, :create_slack_report] do |_task, args|
+ report = QA::Tools::ReliableReport.new(args[:run_type] || "package-and-qa", args[:range])
+
+ report.show_top_unstable
+ report.notify_top_unstable if args[:create_slack_report] == 'true'
+end
+# rubocop:enable Rails/RakeEnvironment