diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-10-20 08:43:02 +0000 |
commit | d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb (patch) | |
tree | 2341ef426af70ad1e289c38036737e04b0aa5007 /qa | |
parent | d6e514dd13db8947884cd58fe2a9c2a063400a9b (diff) | |
download | gitlab-ce-d9ab72d6080f594d0b3cae15f14b3ef2c6c638cb.tar.gz |
Add latest changes from gitlab-org/gitlab@14-4-stable-eev14.4.0-rc42
Diffstat (limited to 'qa')
78 files changed, 2018 insertions, 425 deletions
diff --git a/qa/Gemfile b/qa/Gemfile index cc2355cdfa3..ee90d049d7b 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -3,13 +3,13 @@ source 'https://rubygems.org' gem 'gitlab-qa', require: 'gitlab/qa' -gem 'activesupport', '~> 6.1.3.2' # This should stay in sync with the root's Gemfile -gem 'allure-rspec', '~> 2.14.5' +gem 'activesupport', '~> 6.1.4.1' # This should stay in sync with the root's Gemfile +gem 'allure-rspec', '~> 2.15.0' gem 'capybara', '~> 3.35.0' gem 'capybara-screenshot', '~> 1.0.23' gem 'rake', '~> 12.3.3' gem 'rspec', '~> 3.10' -gem 'selenium-webdriver', '~> 4.0.0.beta4' +gem 'selenium-webdriver', '~> 4.0.0.rc1' 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' @@ -26,7 +26,7 @@ gem 'webdrivers', '~> 4.6' gem 'zeitwerk', '~> 2.4' gem 'influxdb-client', '~> 1.17' -gem 'chemlab', '~> 0.7' +gem 'chemlab', '~> 0.9' gem 'chemlab-library-www-gitlab-com', '~> 0.1' gem 'deprecation_toolkit', '~> 1.5.1', require: false diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 5f33afaa77b..153a141d3fd 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: abstract_type (0.0.7) - activesupport (6.1.3.2) + activesupport (6.1.4.1) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -19,10 +19,10 @@ GEM rack-test (>= 1.1.0, < 2.0) rest-client (>= 2.0.2, < 3.0) rspec (~> 3.8) - allure-rspec (2.14.5) - allure-ruby-commons (= 2.14.5) + allure-rspec (2.15.0) + allure-ruby-commons (= 2.15.0) rspec-core (>= 3.8, < 4) - allure-ruby-commons (2.14.5) + allure-ruby-commons (2.15.0) mime-types (>= 3.3, < 4) oj (>= 3.10, < 4) require_all (>= 2, < 4) @@ -41,7 +41,7 @@ GEM capybara-screenshot (1.0.23) capybara (>= 1.0, < 4) launchy - chemlab (0.7.2) + chemlab (0.9.1) colorize (~> 0.8) i18n (~> 1.8) rake (>= 12, < 14) @@ -81,10 +81,33 @@ GEM faraday-net_http (1.0.1) faraday-net_http_persistent (1.2.0) faraday-patron (1.0.0) - gitlab-qa (4.0.0) + ffi (1.15.4) + ffi-compiler (1.0.1) + ffi (>= 1.0.0) + rake + gitlab (4.16.1) + httparty (~> 0.14, >= 0.14.0) + terminal-table (~> 1.5, >= 1.5.1) + gitlab-qa (7.9.1) + activesupport (~> 6.1) + gitlab (~> 4.16.1) + http (= 4.3.0) + nokogiri (~> 1.10) + table_print (= 1.5.7) + http (4.3.0) + addressable (~> 2.3) + http-cookie (~> 1.0) + http-form_data (~> 2.2) + http-parser (~> 1.2.0) http-accept (1.7.0) http-cookie (1.0.3) 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) + mime-types (~> 3.0) + multi_xml (>= 0.5.2) i18n (1.8.10) concurrent-ruby (~> 1.0) ice_nine (0.11.2) @@ -104,6 +127,7 @@ GEM mini_mime (1.1.0) mini_portile2 (2.5.3) minitest (5.14.4) + multi_xml (0.6.0) multipart-post (2.1.1) netrc (0.11.0) nokogiri (1.11.7) @@ -112,7 +136,7 @@ GEM octokit (4.21.0) faraday (>= 0.9) sawyer (~> 0.8.0, >= 0.5.3) - oj (3.13.2) + oj (3.13.8) parallel (1.19.2) parallel_tests (2.29.0) parallel @@ -135,7 +159,7 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rake (12.3.3) - regexp_parser (1.8.2) + regexp_parser (2.1.1) require_all (3.0.0) rest-client (2.1.0) http-accept (>= 1.7.0, < 2.0) @@ -174,11 +198,14 @@ GEM sawyer (0.8.2) addressable (>= 2.3.5) faraday (> 0.8, < 2.0) - selenium-webdriver (4.0.0.beta4) + selenium-webdriver (4.0.0.rc1) childprocess (>= 0.5, < 5.0) rexml (~> 3.2) rubyzip (>= 1.2.2) systemu (2.6.5) + table_print (1.5.7) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) thread_safe (0.3.6) timecop (0.9.1) tzinfo (2.0.4) @@ -186,6 +213,7 @@ GEM unf (0.1.4) unf_ext unf_ext (0.0.7.7) + unicode-display_width (1.8.0) unparser (0.4.7) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) @@ -211,12 +239,12 @@ PLATFORMS ruby DEPENDENCIES - activesupport (~> 6.1.3.2) + activesupport (~> 6.1.4.1) airborne (~> 0.3.4) - allure-rspec (~> 2.14.5) + allure-rspec (~> 2.15.0) capybara (~> 3.35.0) capybara-screenshot (~> 1.0.23) - chemlab (~> 0.7) + chemlab (~> 0.9) chemlab-library-www-gitlab-com (~> 0.1) deprecation_toolkit (~> 1.5.1) faker (~> 2.19, >= 2.19.0) @@ -235,7 +263,7 @@ DEPENDENCIES rspec-retry (~> 0.6.1) rspec_junit_formatter (~> 0.4.1) ruby-debug-ide (~> 0.7.0) - selenium-webdriver (~> 4.0.0.beta4) + selenium-webdriver (~> 4.0.0.rc1) timecop (~> 0.9.1) webdrivers (~> 4.6) zeitwerk (~> 2.4) diff --git a/qa/chemlab-library-gitlab.gemspec b/qa/chemlab-library-gitlab.gemspec index 908aad01850..34a55ba8927 100644 --- a/qa/chemlab-library-gitlab.gemspec +++ b/qa/chemlab-library-gitlab.gemspec @@ -4,7 +4,7 @@ $:.unshift(File.expand_path('lib', __dir__)) Gem::Specification.new do |spec| spec.name = 'chemlab-library-gitlab' - spec.version = '0.1.1' + spec.version = '0.3.0' spec.authors = ['GitLab Quality'] spec.email = ['quality@gitlab.com'] @@ -18,5 +18,5 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] - spec.add_runtime_dependency 'chemlab', '~> 0.7' + spec.add_runtime_dependency 'chemlab', '~> 0.9' end diff --git a/qa/lib/gitlab.rb b/qa/lib/gitlab.rb index d0d1d535114..4418e51facb 100644 --- a/qa/lib/gitlab.rb +++ b/qa/lib/gitlab.rb @@ -1,19 +1,30 @@ # frozen_string_literal: true +require 'chemlab/library' + # 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 diff --git a/qa/lib/gitlab/page/admin/dashboard.rb b/qa/lib/gitlab/page/admin/dashboard.rb new file mode 100644 index 00000000000..f1a732f8fac --- /dev/null +++ b/qa/lib/gitlab/page/admin/dashboard.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Admin + class Dashboard < Chemlab::Page + path '/admin' + + h2 :users_in_license + h2 :billable_users + h3 :number_of_users + end + end + end +end diff --git a/qa/lib/gitlab/page/admin/dashboard.stub.rb b/qa/lib/gitlab/page/admin/dashboard.stub.rb new file mode 100644 index 00000000000..820acf79b9b --- /dev/null +++ b/qa/lib/gitlab/page/admin/dashboard.stub.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Admin + module Dashboard + # @note Defined as +h2 :users_in_license+ + # @return [String] The text content or value of +users_in_license+ + def users_in_license + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Dashboard.perform do |dashboard| + # expect(dashboard.users_in_license_element).to exist + # end + # @return [Watir::H2] The raw +H2+ element + def users_in_license_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Dashboard.perform do |dashboard| + # expect(dashboard).to be_users_in_license + # end + # @return [Boolean] true if the +users_in_license+ element is present on the page + def users_in_license? + # 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::Dashboard.perform do |dashboard| + # expect(dashboard.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::Dashboard.perform do |dashboard| + # expect(dashboard).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 +h3 :number_of_users+ + # @return [String] The text content or value of +number_of_users+ + def number_of_users + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Dashboard.perform do |dashboard| + # expect(dashboard.number_of_users_element).to exist + # end + # @return [Watir::H3] The raw +H3+ element + def number_of_users_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Dashboard.perform do |dashboard| + # expect(dashboard).to be_number_of_users + # end + # @return [Boolean] true if the +number_of_users+ element is present on the page + def number_of_users? + # This is a stub, used for indexing. The method is dynamically generated. + end + end + end + end +end diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb new file mode 100644 index 00000000000..0f7c6b4c211 --- /dev/null +++ b/qa/lib/gitlab/page/admin/subscription.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Admin + class Subscription < Chemlab::Page + path '/admin/subscription' + + h2 :users_in_subscription + end + end + end +end diff --git a/qa/lib/gitlab/page/admin/subscription.stub.rb b/qa/lib/gitlab/page/admin/subscription.stub.rb new file mode 100644 index 00000000000..51f23e7f0d0 --- /dev/null +++ b/qa/lib/gitlab/page/admin/subscription.stub.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Admin + module Subscription + # @note Defined as +h2 :users_in_subscription+ + # @return [String] The text content or value of +users_in_subscription+ + def users_in_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_in_subscription_element).to exist + # end + # @return [Watir::H2] The raw +H2+ element + def users_in_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_in_subscription + # end + # @return [Boolean] true if the +users_in_subscription+ element is present on the page + def users_in_subscription? + # This is a stub, used for indexing. The method is dynamically generated. + end + end + end + end +end diff --git a/qa/lib/gitlab/page/group/settings/usage_quota.stub.rb b/qa/lib/gitlab/page/group/settings/usage_quota.stub.rb new file mode 100644 index 00000000000..192e71e6c90 --- /dev/null +++ b/qa/lib/gitlab/page/group/settings/usage_quota.stub.rb @@ -0,0 +1,227 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Group + module Settings + module UsageQuota + # @note Defined as +link :pipeline_tab+ + # Clicks +pipeline_tab+ + def pipeline_tab + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.pipeline_tab_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def pipeline_tab_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_pipeline_tab + # end + # @return [Boolean] true if the +pipeline_tab+ element is present on the page + def pipeline_tab? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +link :storage_tab+ + # Clicks +storage_tab+ + def storage_tab + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.storage_tab_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def storage_tab_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_storage_tab + # end + # @return [Boolean] true if the +storage_tab+ element is present on the page + def storage_tab? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +link :buy_ci_minutes+ + # Clicks +buy_ci_minutes+ + def buy_ci_minutes + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.buy_ci_minutes_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def buy_ci_minutes_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_buy_ci_minutes + # end + # @return [Boolean] true if the +buy_ci_minutes+ element is present on the page + def buy_ci_minutes? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +link :buy_storage+ + # Clicks +buy_storage+ + def buy_storage + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.buy_storage_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def buy_storage_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_buy_storage + # end + # @return [Boolean] true if the +buy_storage+ element is present on the page + def buy_storage? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +strong :additional_minutes+ + # @return [String] The text content or value of +additional_minutes+ + def additional_minutes + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.additional_minutes_element).to exist + # end + # @return [Watir::Strong] The raw +Strong+ element + def additional_minutes_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_additional_minutes + # end + # @return [Boolean] true if the +additional_minutes+ element is present on the page + def additional_minutes? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +div :additional_minutes_usage+ + # @return [String] The text content or value of +additional_minutes_usage+ + def additional_minutes_usage + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.additional_minutes_usage_element).to exist + # end + # @return [Watir::Div] The raw +Div+ element + def additional_minutes_usage_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_additional_minutes_usage + # end + # @return [Boolean] true if the +additional_minutes_usage+ element is present on the page + def additional_minutes_usage? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +strong :plan_minutes+ + # @return [String] The text content or value of +plan_minutes+ + def plan_minutes + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.plan_minutes_element).to exist + # end + # @return [Watir::Strong] The raw +Strong+ element + def plan_minutes_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_plan_minutes + # end + # @return [Boolean] true if the +plan_minutes+ element is present on the page + def plan_minutes? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +div :plan_minutes_usage+ + # @return [String] The text content or value of +plan_minutes_usage+ + def plan_minutes_usage + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.plan_minutes_usage_element).to exist + # end + # @return [Watir::Div] The raw +Div+ element + def plan_minutes_usage_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_plan_minutes_usage + # end + # @return [Boolean] true if the +plan_minutes_usage+ element is present on the page + def plan_minutes_usage? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +div :purchase_successful_alert+ + # @return [String] The text content or value of +purchase_successful_alert+ + def purchase_successful_alert + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota.purchase_successful_alert_element).to exist + # end + # @return [Watir::Div] The raw +Div+ element + def purchase_successful_alert_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::UsageQuota.perform do |usage_quota| + # expect(usage_quota).to be_purchase_successful_alert + # end + # @return [Boolean] true if the +purchase_successful_alert+ element is present on the page + def purchase_successful_alert? + # This is a stub, used for indexing. The method is dynamically generated. + end + end + end + end + end +end diff --git a/qa/lib/gitlab/page/group/settings/usage_quotas.rb b/qa/lib/gitlab/page/group/settings/usage_quotas.rb new file mode 100644 index 00000000000..455a695f703 --- /dev/null +++ b/qa/lib/gitlab/page/group/settings/usage_quotas.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Group + module Settings + class UsageQuotas < Chemlab::Page + link :pipeline_tab, id: 'pipelines-quota' + link :storage_tab, id: 'storage-quota' + link :buy_ci_minutes, text: 'Buy additional minutes' + link :buy_storage, text: /Purchase more storage/ + strong :additional_minutes, text: 'Additional minutes' + div(:additional_minutes_usage) { additional_minutes_element.following_sibling.span } + div :purchase_successful_alert, text: /You have successfully purchased CI minutes/ + + def plan_minutes_limits + plan_minutes_usage[%r{([^/ ]+)$}] + end + + def additional_limits + additional_minutes_usage[%r{([^/ ]+)$}] + end + end + end + end + end +end diff --git a/qa/lib/gitlab/page/main/login.rb b/qa/lib/gitlab/page/main/login.rb index 9f20a040550..de05df1a086 100644 --- a/qa/lib/gitlab/page/main/login.rb +++ b/qa/lib/gitlab/page/main/login.rb @@ -10,11 +10,27 @@ module Gitlab text_field :password_field button :sign_in_button - def sign_in_as(username:, password:) + button :accept_terms, text: 'Accept terms' + + # password change tab + text_field :password_confirmation_field + button :change_password_button + + # Sign in using a given username and password + # @note this will also automatically accept terms if prompted + # @param [String] username the username to sign in with + # @param [String] password the password to sign in with + # @example + # Page::Main::Login.perform do |login| + # login.sign_in_as(username: 'username', password: 'password') + # login.sign_in_as(username: 'username', password: 'password', accept_terms: false) + # end + def sign_in_as(username:, password:, accept_terms: true) self.login_field = username self.password_field = password sign_in_button + self.accept_terms if accept_terms && accept_terms? end end end diff --git a/qa/lib/gitlab/page/main/login.stub.rb b/qa/lib/gitlab/page/main/login.stub.rb index a4cef291616..a819ca4bcc8 100644 --- a/qa/lib/gitlab/page/main/login.stub.rb +++ b/qa/lib/gitlab/page/main/login.stub.rb @@ -95,6 +95,88 @@ module Gitlab def sign_in_button? # This is a stub, used for indexing. The method is dynamically generated. end + + # @note Defined as +button :accept_terms+ + # Clicks +accept_terms+ + def accept_terms + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Login.perform do |login| + # expect(login.accept_terms_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def accept_terms_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Login.perform do |login| + # expect(login).to be_accept_terms + # end + # @return [Boolean] true if the +accept_terms+ element is present on the page + def accept_terms? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :password_confirmation_field+ + # @return [String] The text content or value of +password_confirmation_field+ + def password_confirmation_field + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of password_confirmation_field + # @example + # Gitlab::Page::Main::Login.perform do |login| + # login.password_confirmation_field = 'value' + # end + # @param value [String] The value to set. + def password_confirmation_field=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Login.perform do |login| + # expect(login.password_confirmation_field_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def password_confirmation_field_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Login.perform do |login| + # expect(login).to be_password_confirmation_field + # end + # @return [Boolean] true if the +password_confirmation_field+ element is present on the page + def password_confirmation_field? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +button :change_password_button+ + # Clicks +change_password_button+ + def change_password_button + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Login.perform do |login| + # expect(login.change_password_button_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def change_password_button_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Login.perform do |login| + # expect(login).to be_change_password_button + # end + # @return [Boolean] true if the +change_password_button+ element is present on the page + def change_password_button? + # This is a stub, used for indexing. The method is dynamically generated. + end end end end diff --git a/qa/lib/gitlab/page/main/sign_up.rb b/qa/lib/gitlab/page/main/sign_up.rb new file mode 100644 index 00000000000..85d7f482461 --- /dev/null +++ b/qa/lib/gitlab/page/main/sign_up.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Main + class SignUp < Chemlab::Page + path '/users/sign_up' + + # TODO: Refactor data-qa-selectors to be more terse + text_field :first_name, 'data-qa-selector': 'new_user_first_name_field' + text_field :last_name, 'data-qa-selector': 'new_user_last_name_field' + + text_field :username, 'data-qa-selector': 'new_user_username_field' + + text_field :email, 'data-qa-selector': 'new_user_email_field' + text_field :password, 'data-qa-selector': 'new_user_password_field' + + button :register, 'data-qa-selector': 'new_user_register_button' + + # Register a user + # @param [Resource::User] user the user to register + def register_user(user) + raise ArgumentError, 'User must be of type Resource::User' unless user.is_a? ::QA::Resource::User + + self.first_name = user.first_name + self.last_name = user.last_name + self.username = user.username + self.email = user.email + self.password = user.password + + self.register + end + end + end + end +end diff --git a/qa/lib/gitlab/page/main/sign_up.stub.rb b/qa/lib/gitlab/page/main/sign_up.stub.rb new file mode 100644 index 00000000000..881bd922c45 --- /dev/null +++ b/qa/lib/gitlab/page/main/sign_up.stub.rb @@ -0,0 +1,203 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Main + module SignUp + # @note Defined as +text_field :first_name+ + # @return [String] The text content or value of +first_name+ + def first_name + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of first_name + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # sign_up.first_name = 'value' + # end + # @param value [String] The value to set. + def first_name=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up.first_name_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def first_name_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up).to be_first_name + # end + # @return [Boolean] true if the +first_name+ element is present on the page + def first_name? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :last_name+ + # @return [String] The text content or value of +last_name+ + def last_name + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of last_name + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # sign_up.last_name = 'value' + # end + # @param value [String] The value to set. + def last_name=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up.last_name_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def last_name_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up).to be_last_name + # end + # @return [Boolean] true if the +last_name+ element is present on the page + def last_name? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :username+ + # @return [String] The text content or value of +username+ + def username + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of username + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # sign_up.username = 'value' + # end + # @param value [String] The value to set. + def username=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up.username_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def username_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up).to be_username + # end + # @return [Boolean] true if the +username+ element is present on the page + def username? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :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 + + # Set the value of email + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # sign_up.email = 'value' + # end + # @param value [String] The value to set. + def email=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up.email_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def email_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up).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 +text_field :password+ + # @return [String] The text content or value of +password+ + def password + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of password + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # sign_up.password = 'value' + # end + # @param value [String] The value to set. + def password=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up.password_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def password_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up).to be_password + # end + # @return [Boolean] true if the +password+ element is present on the page + def password? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +button :register+ + # Clicks +register+ + def register + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up.register_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def register_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::SignUp.perform do |sign_up| + # expect(sign_up).to be_register + # end + # @return [Boolean] true if the +register+ element is present on the page + def register? + # This is a stub, used for indexing. The method is dynamically generated. + end + end + end + end +end diff --git a/qa/lib/gitlab/page/subscriptions/new.rb b/qa/lib/gitlab/page/subscriptions/new.rb index 4c0e5446444..6e3cb45fd29 100644 --- a/qa/lib/gitlab/page/subscriptions/new.rb +++ b/qa/lib/gitlab/page/subscriptions/new.rb @@ -6,10 +6,11 @@ module Gitlab class New < Chemlab::Page path '/subscriptions/new' - # Subscription Details + # Purchase Details select :plan_name select :group_name text_field :number_of_users + text_field :quantity button :continue_to_billing, text: /Continue to billing/ # Billing address @@ -35,6 +36,10 @@ module Gitlab # Confirmation button :confirm_purchase, text: /Confirm purchase/ + + # Order Summary + div :selected_plan, 'data-testid': 'selected-plan' + div :order_total, 'data-testid': 'total-amount' end end end diff --git a/qa/qa/page/component/issue_board/show.rb b/qa/qa/page/component/issue_board/show.rb index 1c1f7ab17f3..4b842412c0f 100644 --- a/qa/qa/page/component/issue_board/show.rb +++ b/qa/qa/page/component/issue_board/show.rb @@ -48,7 +48,7 @@ module QA # with the attribute `data-qa-selector` since such element is not unique when the # `is-focused` class is not set, and it was not possible to find a better solution. def focused_board - find('.issue-boards-content.js-focus-mode-board.is-focused') + find('.js-focus-mode-board.is-focused') end def boards_dropdown diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb index 73f41e0aa51..ad264bd6d56 100644 --- a/qa/qa/page/component/snippet.rb +++ b/qa/qa/page/component/snippet.rb @@ -79,6 +79,11 @@ module QA element :default_actions_container element :copy_contents_button end + + base.view 'app/views/layouts/nav/_breadcrumbs.html.haml' do + element :breadcrumb_links_content + element :breadcrumb_sub_title_content + end end def has_snippet_title?(snippet_title) @@ -249,6 +254,12 @@ module QA raise ElementNotFound, "Comment did not appear as expected" end end + + def snippet_id + within_element(:breadcrumb_links_content) do + find_element(:breadcrumb_sub_title_content).text.delete_prefix('$') + end + end end end end diff --git a/qa/qa/page/component/web_ide/web_terminal_panel.rb b/qa/qa/page/component/web_ide/web_terminal_panel.rb new file mode 100644 index 00000000000..80f83bcff7c --- /dev/null +++ b/qa/qa/page/component/web_ide/web_terminal_panel.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module WebIDE + module WebTerminalPanel + extend QA::Page::PageConcern + + def self.prepended(base) + super + + base.class_eval do + view 'app/assets/javascripts/ide/components/panes/collapsible_sidebar.vue' do + element :ide_right_sidebar, %q(:data-qa-selector="`ide_${side}_sidebar`") # rubocop:disable QA/ElementWithPattern + end + + view 'app/assets/javascripts/ide/components/ide_sidebar_nav.vue' do + element :terminal_tab_button, %q(:data-qa-selector="`${tab.title.toLowerCase()}_tab_button`") # rubocop:disable QA/ElementWithPattern + end + + view 'app/assets/javascripts/ide/components/terminal/empty_state.vue' do + element :start_web_terminal_button + end + + view 'app/assets/javascripts/ide/components/terminal/terminal.vue' do + element :loading_container + element :terminal_screen + end + end + end + + def has_finished_loading? + has_no_element?(:loading_container, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) + end + + def has_terminal_screen? + wait_until(reload: false) do + within_element :terminal_screen do + # The DOM initially just includes the :terminal_screen element + # and then the xterm package dynamically loads when the user + # clicks the Start Web Terminal button. If it loads succesfully + # an element with the class `xterm` is added to the DOM. + # The xterm is a third-party library, so we can't add a selector + find(".xterm") + end + end + end + + def start_web_terminal + within_element :ide_right_sidebar do + click_element :terminal_tab_button + end + + click_element :start_web_terminal_button + + has_element? :loading_container, text: "Starting" + end + end + end + end + end +end diff --git a/qa/qa/page/group/bulk_import.rb b/qa/qa/page/group/bulk_import.rb index b9497aeb6e5..a62823f3469 100644 --- a/qa/qa/page/group/bulk_import.rb +++ b/qa/qa/page/group/bulk_import.rb @@ -33,7 +33,12 @@ module QA within_element(:import_item, source_group: source_group_name) do click_element(:target_namespace_selector_dropdown) click_element(:target_group_dropdown_item, group_name: target_group_name) - click_element(:import_group_button) + + retry_until 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) + end end end diff --git a/qa/qa/page/group/dependency_proxy.rb b/qa/qa/page/group/dependency_proxy.rb index f637c79cffc..fa37e8eac83 100644 --- a/qa/qa/page/group/dependency_proxy.rb +++ b/qa/qa/page/group/dependency_proxy.rb @@ -4,19 +4,10 @@ module QA module Page module Group class DependencyProxy < QA::Page::Base - view 'app/views/groups/dependency_proxies/show.html.haml' do - element :dependency_proxy_setting_toggle - end - - view 'app/views/groups/dependency_proxies/_url.html.haml' do + view 'app/assets/javascripts/packages_and_registries/dependency_proxy/app.vue' do element :dependency_proxy_count end - def has_dependency_proxy_enabled? - toggle = find_element(:dependency_proxy_setting_toggle) - toggle[:class].include?('is-checked') - end - def has_blob_count?(blob_text) has_element?(:dependency_proxy_count, text: blob_text) end diff --git a/qa/qa/page/group/settings/package_registries.rb b/qa/qa/page/group/settings/package_registries.rb index 8a2802b0035..5c93c0d6222 100644 --- a/qa/qa/page/group/settings/package_registries.rb +++ b/qa/qa/page/group/settings/package_registries.rb @@ -7,15 +7,17 @@ module QA class PackageRegistries < QA::Page::Base include ::QA::Page::Settings::Common - view 'app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue' do + view 'app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue' do element :package_registry_settings_content - end - - view 'app/assets/javascripts/packages_and_registries/settings/group/components/group_settings_app.vue' do element :allow_duplicates_toggle element :allow_duplicates_label end + view 'app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue' do + element :dependency_proxy_settings_content + element :dependency_proxy_setting_toggle + end + def set_allow_duplicates_disabled expand_content :package_registry_settings_content do click_element(:allow_duplicates_toggle) if duplicates_enabled? @@ -35,6 +37,15 @@ module QA def duplicates_disabled? has_element?(:allow_duplicates_label, text: 'Do not allow duplicates') end + + def has_dependency_proxy_enabled? + expand_content :dependency_proxy_settings_content do + within_element :dependency_proxy_setting_toggle do + toggle = find('button.gl-toggle') + toggle[:class].include?('is-checked') + end + end + end end end end diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb index 71e51ddd504..bcc60a8275d 100644 --- a/qa/qa/page/merge_request/new.rb +++ b/qa/qa/page/merge_request/new.rb @@ -20,6 +20,13 @@ module QA element :file_name_content end + def has_secure_description?(scanner_name) + scanner_url_name = scanner_name.downcase.tr('_', '-') + "Configure #{scanner_name} in `.gitlab-ci.yml` using the GitLab managed template. You can " \ + "[add variable overrides](https://docs.gitlab.com/ee/user/application_security/#{scanner_url_name}/#customizing-the-#{scanner_url_name}-settings) " \ + "to customize #{scanner_name} settings." + end + def create_merge_request click_element(:issuable_create_button, Page::MergeRequest::Show) end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 1d8d9ed6859..d4fa3b38f02 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -25,6 +25,11 @@ module QA view 'app/assets/javascripts/diffs/components/compare_versions.vue' do element :target_version_dropdown + element :file_tree_button + end + + view 'app/assets/javascripts/diffs/components/tree_list.vue' do + element :file_tree_container end view 'app/assets/javascripts/diffs/components/diff_file_header.vue' do @@ -93,7 +98,6 @@ module QA end view 'app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue' do - element :apply_suggestions_batch_button element :add_suggestion_batch_button end @@ -187,11 +191,17 @@ module QA end def has_file?(file_name) - has_element?(:file_name_content, text: file_name) + open_file_tree + has_element?(:file_name_content, file_name: file_name) end def has_no_file?(file_name) - has_no_element?(:file_name_content, text: file_name) + open_file_tree + has_no_element?(:file_name_content, file_name: file_name) + end + + def open_file_tree + click_element(:file_tree_button) unless has_element?(:file_tree_container) end def has_merge_button? @@ -202,7 +212,7 @@ module QA def has_pipeline_status?(text) # Pipelines can be slow, so we wait a bit longer than the usual 10 seconds - wait_until(sleep_interval: 5, reload: false) do + wait_until(max_duration: 120, sleep_interval: 5, reload: true) do has_element?(:merge_request_pipeline_info_content, text: text, wait: 15 ) end end @@ -288,13 +298,11 @@ module QA end def merge_immediately! - merge_moment_dropdown_found = has_element?(:merge_moment_dropdown, wait: 0) - - if merge_moment_dropdown_found - click_element(:merge_moment_dropdown) - click_element(:merge_immediately_menu_item) + if has_element?(:merge_moment_dropdown) + click_element(:merge_moment_dropdown, skip_finished_loading_check: true) + click_element(:merge_immediately_menu_item, skip_finished_loading_check: true) else - click_element(:merge_button) + click_element(:merge_button, skip_finished_loading_check: true) end end @@ -355,10 +363,6 @@ module QA all_elements(:add_suggestion_batch_button, minimum: 1).first.click end - def apply_suggestions_batch - all_elements(:apply_suggestions_batch_button, minimum: 1).first.click - end - def cherry_pick! click_element(:cherry_pick_button, Page::Component::CommitModal) click_element(:submit_commit_button) diff --git a/qa/qa/page/profile/two_factor_auth.rb b/qa/qa/page/profile/two_factor_auth.rb index a0dd230d8ab..63593bf0482 100644 --- a/qa/qa/page/profile/two_factor_auth.rb +++ b/qa/qa/page/profile/two_factor_auth.rb @@ -11,6 +11,7 @@ module QA view 'app/views/profiles/two_factor_auths/show.html.haml' do element :otp_secret_content element :pin_code_field + element :current_password_field element :register_2fa_app_button end @@ -33,6 +34,10 @@ module QA fill_element(:pin_code_field, pin_code) end + def set_current_password(password) + fill_element(:current_password_field, password) + end + def click_register_2fa_app_button click_element :register_2fa_app_button end diff --git a/qa/qa/page/project/artifact/show.rb b/qa/qa/page/project/artifact/show.rb index 437363d4a98..5e05eec79f0 100644 --- a/qa/qa/page/project/artifact/show.rb +++ b/qa/qa/page/project/artifact/show.rb @@ -9,8 +9,10 @@ module QA element :directory_name_link end - def go_to_directory(name) - click_element(:directory_name_link, directory_name: name) + def go_to_directory(name, retry_attempts = 1) + retry_on_exception(max_attempts: retry_attempts, reload: true, sleep_interval: 10) do + click_element(:directory_name_link, directory_name: name) + end end end end diff --git a/qa/qa/page/project/import/repo_by_url.rb b/qa/qa/page/project/import/repo_by_url.rb index 0e7524a181a..4a8d08d6499 100644 --- a/qa/qa/page/project/import/repo_by_url.rb +++ b/qa/qa/page/project/import/repo_by_url.rb @@ -5,10 +5,9 @@ module QA module Project module Import class RepoByURL < Page::Base - include Page::Component::Select2 - - view 'app/views/projects/_new_project_fields.html.haml' do + view 'app/assets/javascripts/projects/new/components/new_project_url_select.vue' do element :select_namespace_dropdown + element :select_namespace_dropdown_search_field end def import!(gitlab_repo_path, name) @@ -33,8 +32,15 @@ module QA end def choose_test_namespace - find('.js-select-namespace').click - search_and_select(Runtime::Namespace.path) + choose_namespace(Runtime::Namespace.path) + end + + def choose_namespace(namespace) + retry_on_exception do + click_element :select_namespace_dropdown + fill_element :select_namespace_dropdown_search_field, namespace + click_button namespace + end end def click_create_button diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 06e476f009a..3ecdabeeed2 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -5,7 +5,6 @@ module QA module Project class New < Page::Base include Page::Component::Project::Templates - include Page::Component::Select2 include Page::Component::VisibilitySetting include Layout::Flash @@ -14,7 +13,6 @@ module QA view 'app/views/projects/_new_project_fields.html.haml' do element :initialize_with_readme_checkbox - element :project_namespace_select 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 @@ -28,6 +26,11 @@ module QA element :template_option_row end + view 'app/assets/javascripts/projects/new/components/new_project_url_select.vue' do + element :select_namespace_dropdown + element :select_namespace_dropdown_search_field + end + view 'app/assets/javascripts/vue_shared/new_namespace/components/welcome.vue' do element :panel_link end @@ -46,8 +49,9 @@ module QA def choose_namespace(namespace) retry_on_exception do - click_element :project_namespace_select unless dropdown_open? - search_and_select(namespace) + click_element :select_namespace_dropdown + fill_element :select_namespace_dropdown_search_field, namespace + click_button namespace end end diff --git a/qa/qa/page/project/packages/show.rb b/qa/qa/page/project/packages/show.rb index 59e9a3752c7..4872c0bc705 100644 --- a/qa/qa/page/project/packages/show.rb +++ b/qa/qa/page/project/packages/show.rb @@ -5,7 +5,7 @@ module QA module Project module Packages class Show < QA::Page::Base - view 'app/assets/javascripts/packages/details/components/app.vue' do + view 'app/assets/javascripts/packages_and_registries/package_registry/components/details/app.vue' do element :delete_button element :delete_modal_button element :package_information_content diff --git a/qa/qa/page/project/registry/show.rb b/qa/qa/page/project/registry/show.rb index 03c547fc8b5..f2472a83401 100644 --- a/qa/qa/page/project/registry/show.rb +++ b/qa/qa/page/project/registry/show.rb @@ -10,6 +10,10 @@ module QA end view 'app/assets/javascripts/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 element :tag_delete_button end @@ -30,6 +34,7 @@ module QA end def click_delete + click_element(:more_actions_menu) click_element(:tag_delete_button) find_button('Delete').click end diff --git a/qa/qa/page/project/secure/configuration_form.rb b/qa/qa/page/project/secure/configuration_form.rb index 73d1601b61e..3e89a57e870 100644 --- a/qa/qa/page/project/secure/configuration_form.rb +++ b/qa/qa/page/project/secure/configuration_form.rb @@ -9,19 +9,31 @@ module QA include QA::Page::Settings::Common view 'app/assets/javascripts/security_configuration/components/feature_card.vue' do + element :dependency_scanning_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern element :sast_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern element :sast_enable_button, "`${feature.type}_enable_button`" # rubocop:disable QA/ElementWithPattern + element :dependency_scanning_mr_button, "`${feature.type}_mr_button`" # rubocop:disable QA/ElementWithPattern end def click_sast_enable_button click_element(:sast_enable_button) end + def click_dependency_scanning_mr_button + click_element(:dependency_scanning_mr_button) + end + def has_sast_status?(status_text) within_element(:sast_status) do has_text?(status_text) end end + + def has_dependency_scanning_status?(status_text) + within_element(:dependency_scanning_status) do + has_text?(status_text) + end + end end end end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 78b2db7d723..9c0a3ab691c 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -6,6 +6,7 @@ module QA module WebIDE class Edit < Page::Base prepend Page::Component::WebIDE::Alert + prepend Page::Component::WebIDE::WebTerminalPanel include Page::Component::DropdownFilter view 'app/assets/javascripts/ide/components/activity_bar.vue' do @@ -330,5 +331,3 @@ module QA end end end - -QA::Page::Project::WebIDE::Edit.prepend_mod_with('Page::Component::WebIDE::WebTerminalPanel', namespace: QA) diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index c1533577657..b94fa543b48 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -96,31 +96,35 @@ module QA end def api_post - if api_post_path == "/graphql" - graphql_response = post( - Runtime::API::Request.new(api_client, api_post_path).url, - query: api_post_body) + process_api_response(api_post_to(api_post_path, api_post_body)) + end + + def api_post_to(post_path, post_body) + if post_path == "/graphql" + graphql_response = post(Runtime::API::Request.new(api_client, post_path).url, query: post_body) - flattened_response = flatten_hash(parse_body(graphql_response)) + body = flatten_hash(parse_body(graphql_response)) - unless graphql_response.code == HTTP_STATUS_OK && flattened_response[:errors].empty? - raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{graphql_response.code}) with `#{graphql_response}`." + unless graphql_response.code == HTTP_STATUS_OK && (body[:errors].nil? || body[:errors].empty?) + raise(ResourceFabricationFailedError, <<~MSG) + Fabrication of #{self.class.name} using the API failed (#{graphql_response.code}) with `#{graphql_response}`. + MSG end - flattened_response[:web_url] = flattened_response.delete(:webUrl) - flattened_response[:id] = flattened_response.fetch(:id).split('/')[-1] + body[:id] = body.fetch(:id).split('/').last - process_api_response(flattened_response) + body.transform_keys { |key| key.to_s.underscore.to_sym } else - response = post( - Runtime::API::Request.new(api_client, api_post_path).url, - api_post_body) + response = post(Runtime::API::Request.new(api_client, post_path).url, post_body) unless response.code == HTTP_STATUS_CREATED - raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`." + raise( + ResourceFabricationFailedError, + "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`." + ) end - process_api_response(parse_body(response)) + parse_body(response) end end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index 2848e3ba7d2..a7243b7ebc2 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -100,7 +100,9 @@ module QA attr_writer(name) define_method(name) do - instance_variable_get("@#{name}") || instance_variable_set("@#{name}", populate_attribute(name, block)) + return instance_variable_get("@#{name}") if instance_variable_defined?("@#{name}") + + instance_variable_set("@#{name}", attribute_value(name, block)) end end @@ -121,9 +123,7 @@ module QA return self unless api_resource all_attributes.each do |attribute_name| - api_value = api_resource[attribute_name] - - instance_variable_set("@#{attribute_name}", api_value) if api_value + instance_variable_set("@#{attribute_name}", api_resource[attribute_name]) if api_resource.key?(attribute_name) end self @@ -160,20 +160,17 @@ module QA private - def populate_attribute(name, block) - value = attribute_value(name, block) - - raise NoValueError, "No value was computed for #{name} of #{self.class.name}." unless value - - value - end - def attribute_value(name, block) - api_value = api_resource&.dig(name) + no_api_value = !api_resource&.key?(name) + raise NoValueError, "No value was computed for #{name} of #{self.class.name}." if no_api_value && !block - log_having_both_api_result_and_block(name, api_value) if api_value && block + unless no_api_value + api_value = api_resource[name] + log_having_both_api_result_and_block(name, api_value) if block + return api_value + end - api_value || (block && instance_exec(&block)) + instance_exec(&block) end # Get all defined attributes across all parents diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb index 106d1d5548a..b3814011f2c 100644 --- a/qa/qa/resource/fork.rb +++ b/qa/qa/resource/fork.rb @@ -14,6 +14,7 @@ module QA resource.add_name_uuid = false resource.name = name resource.path_with_namespace = "#{user.username}/#{name}" + resource.api_client = @api_client end end @@ -69,6 +70,12 @@ module QA populate(:project) end + def remove_via_api! + project.remove_via_api! + upstream.remove_via_api! + user.remove_via_api! unless Specs::Helpers::ContextSelector.dot_com? + end + def api_get_path "/projects/#{CGI.escape(path_with_namespace)}" end diff --git a/qa/qa/resource/group_badge.rb b/qa/qa/resource/group_badge.rb new file mode 100644 index 00000000000..fd76f066e8b --- /dev/null +++ b/qa/qa/resource/group_badge.rb @@ -0,0 +1,73 @@ +# frozen_string_literal: true + +module QA + module Resource + class GroupBadge < Base + attributes :id, + :name, + :link_url, + :image_url, + :group + + # API get path + # + # @return [String] + def api_get_path + "/groups/#{CGI.escape(group.full_path)}/badges/#{id}" + end + + # API post path + # + # @return [String] + def api_post_path + "/groups/#{CGI.escape(group.full_path)}/badges" + end + + # Params for label creation + # + # @return [Hash] + def api_post_body + { + link_url: link_url, + image_url: image_url + } + end + + # Override base method as this particular resource does not expose a web_url property + # + # @param [Hash] resource + # @return [String] + def resource_web_url(_resource); end + + # Object comparison + # + # @param [QA::Resource::GroupBadge] other + # @return [Boolean] + def ==(other) + other.is_a?(GroupBadge) && comparable_badge == other.comparable_badge + end + + # Override inspect for a better rspec failure diff output + # + # @return [String] + def inspect + JSON.pretty_generate(comparable_badge) + end + + protected + + # Return subset of fields for comparing badges + # + # @return [Hash] + def comparable_badge + reload! unless api_response + + api_response.slice( + :name, + :link_url, + :image_url + ) + end + end + end +end diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb index a1e5b19f409..932d39675a3 100644 --- a/qa/qa/resource/group_base.rb +++ b/qa/qa/resource/group_base.rb @@ -14,6 +14,22 @@ module QA :name, :full_path + # Get group projects + # + # @return [Array<QA::Resource::Project>] + def projects + parse_body(api_get_from("#{api_get_path}/projects")).map do |project| + Project.init do |resource| + resource.api_client = api_client + resource.group = self + resource.id = project[:id] + resource.name = project[:name] + resource.description = project[:description] + resource.path_with_namespace = project[:path_with_namespace] + end + end + end + # Get group labels # # @return [Array<QA::Resource::GroupLabel>] @@ -46,6 +62,38 @@ module QA end end + # Get group badges + # + # @return [Array<QA::Resource::GroupBadge>] + def badges + parse_body(api_get_from("#{api_get_path}/badges")).map do |badge| + GroupBadge.init do |resource| + resource.api_client = api_client + resource.group = self + resource.id = badge[:id] + resource.name = badge[:name] + resource.link_url = badge[:link_url] + resource.image_url = badge[:image_url] + end + end + end + + # Get group members + # + # @return [Array<QA::Resource::User>] + def members + parse_body(api_get_from("#{api_get_path}/members")).map do |member| + User.init do |resource| + resource.api_client = api_client + resource.id = member[:id] + resource.name = member[:name] + resource.username = member[:username] + resource.email = member[:email] + resource.access_level = member[:access_level] + end + end + end + # API get path # # @return [String] diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb index 4eebbdf0a52..b0579cf37b8 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_browser_ui! + Fork.fabricate_via_api! end attribute :push do @@ -23,8 +23,15 @@ module QA fork.project.visit! - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform(&:create_merge_request) + mr_url = Flow::Login.while_signed_in(as: fork.user) do + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform(&:create_merge_request) + + current_url + end + + Flow::Login.sign_in + visit(mr_url) end def fabricate_via_api! diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb index 924e4206166..d992d7987b4 100644 --- a/qa/qa/resource/personal_access_token.rb +++ b/qa/qa/resource/personal_access_token.rb @@ -8,7 +8,7 @@ module QA attr_accessor :name # The user for which the personal access token is to be created - # This *could* be different than the api_client.user or the api_user provided by the QA::Resource::ApiFabricator module + # This *could* be different than the api_client.user or the api_user provided by the QA::Resource::ApiFabricator attr_writer :user attribute :token @@ -17,7 +17,9 @@ module QA # If Runtime::Env.admin_personal_access_token is provided, fabricate via the API, # else, fabricate via the browser. def fabricate_via_api! - @token = QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username) + QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username).tap do |cached_token| + @token = cached_token if cached_token + end return if @token resource = if Runtime::Env.admin_personal_access_token && !@user.nil? @@ -28,7 +30,7 @@ module QA fabricate! end - QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) + QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) resource end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 5ad55090f8c..3f6a4eee5ac 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -12,13 +12,13 @@ module QA :auto_devops_enabled, :github_personal_access_token, :github_repository_path, - :gitlab_repository_path + :gitlab_repository_path, + :personal_namespace attributes :id, :name, :add_name_uuid, :description, - :personal_namespace, :runners_token, :visibility, :template_name, @@ -31,13 +31,15 @@ module QA end attribute :path_with_namespace do - "#{group.full_path}/#{name}" + "#{personal_namespace || group.full_path}/#{name}" end alias_method :full_path, :path_with_namespace def sandbox_path - group.respond_to?('sandbox') ? "#{group.sandbox.path}/" : '' + return '' if personal_namespace || !group.respond_to?('sandbox') + + "#{group.sandbox.path}/" end attribute :repository_ssh_location do @@ -50,12 +52,12 @@ module QA def initialize @add_name_uuid = true - @personal_namespace = false @description = 'My awesome project' @initialize_with_readme = false @auto_devops_enabled = false @visibility = :public @template_name = nil + @personal_namespace = nil @import = false self.name = "the_awesome_project" @@ -68,7 +70,7 @@ module QA def fabricate! return if @import - if @personal_namespace + if personal_namespace Page::Dashboard::Projects.perform(&:click_new_project_button) else group.visit! @@ -356,6 +358,46 @@ module QA parse_body(response) end + # Object comparison + # + # @param [QA::Resource::Project] other + # @return [Boolean] + def ==(other) + other.is_a?(Project) && comparable_project == other.comparable_project + end + + # Override inspect for a better rspec failure diff output + # + # @return [String] + def inspect + JSON.pretty_generate(comparable_project) + end + + protected + + # Return subset of fields for comparing projects + # + # @return [Hash] + def comparable_project + reload! if api_response.nil? + + api_resource.slice( + :name, + :path, + :description, + :tag_list, + :archived, + :issues_enabled, + :merge_request_enabled, + :wiki_enabled, + :jobs_enabled, + :snippets_enabled, + :shared_runners_enabled, + :request_access_enabled, + :avatar_url + ) + end + private def transform_api_resource(api_resource) diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb index cffeed7a64b..28a0f12b3e3 100644 --- a/qa/qa/resource/project_imported_from_github.rb +++ b/qa/qa/resource/project_imported_from_github.rb @@ -34,7 +34,7 @@ module QA def fabricate_via_api! super rescue ResourceURLMissingError - "#{Runtime::Scenario.gitlab_address}/#{group.full_path}/#{name}" + "#{Runtime::Scenario.gitlab_address}/#{full_path}" end def api_post_path @@ -49,7 +49,7 @@ module QA { repo_id: github_repo_id, new_name: name, - target_namespace: group.full_path, + target_namespace: @personal_namespace || group.full_path, personal_access_token: github_personal_access_token, ci_cd_only: false } diff --git a/qa/qa/resource/project_snippet.rb b/qa/qa/resource/project_snippet.rb index 9ab4612d117..9a22966efdb 100644 --- a/qa/qa/resource/project_snippet.rb +++ b/qa/qa/resource/project_snippet.rb @@ -30,6 +30,8 @@ module QA new_snippet.click_create_snippet_button end + + @id = Page::Project::Snippet::Show.perform(&:snippet_id) end def api_get_path diff --git a/qa/qa/resource/snippet.rb b/qa/qa/resource/snippet.rb index a94ae02b8fd..a79e8c7de6b 100644 --- a/qa/qa/resource/snippet.rb +++ b/qa/qa/resource/snippet.rb @@ -22,6 +22,10 @@ module QA end def fabricate! + Page::Main::Menu.perform do |menu| + menu.go_to_menu_dropdown_option(:snippets_link) + end + Page::Dashboard::Snippet::Index.perform(&:go_to_new_snippet_page) Page::Dashboard::Snippet::New.perform do |new_page| @@ -38,6 +42,8 @@ module QA end new_page.click_create_snippet_button end + + @id = Page::Dashboard::Snippet::Show.perform(&:snippet_id) end def fabricate_via_api! diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index 811ce5e0505..ed4ea057484 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -7,13 +7,18 @@ module QA attr_reader :unique_id attr_writer :username, :password - attr_accessor :admin, :provider, :extern_uid, :expect_fabrication_success, :hard_delete_on_api_removal - - attribute :id - attribute :name - attribute :first_name - attribute :last_name - attribute :email + attr_accessor :admin, + :provider, + :extern_uid, + :expect_fabrication_success, + :hard_delete_on_api_removal, + :access_level + + attributes :id, + :name, + :first_name, + :last_name, + :email def initialize @admin = false @@ -123,6 +128,10 @@ module QA "/users/#{id}/block" end + def api_approve_path + "/users/#{id}/approve" + end + def api_post_body { admin: admin, @@ -148,6 +157,13 @@ module QA end end + def approve! + response = post(Runtime::API::Request.new(api_client, api_approve_path).url, nil) + return if response.code == 201 + + raise ResourceUpdateFailedError, "Failed to approve user. Request returned (#{response.code}): `#{response}`" + end + def block! response = post(Runtime::API::Request.new(api_client, api_block_path).url, nil) return if response.code == HTTP_STATUS_CREATED diff --git a/qa/qa/runtime/allure_report.rb b/qa/qa/runtime/allure_report.rb index 5b0456dc607..5f628050f3b 100644 --- a/qa/qa/runtime/allure_report.rb +++ b/qa/qa/runtime/allure_report.rb @@ -5,6 +5,8 @@ require 'active_support/core_ext/enumerable' module QA module Runtime class AllureReport + extend QA::Support::API + class << self # Configure allure reports # @@ -77,27 +79,21 @@ module QA end end - # Custom environment info hash + # Gitlab version and revision information # # @return [Hash] def environment_info - %w[ - CI_COMMIT_SHA - CI_MERGE_REQUEST_SOURCE_BRANCH_SHA - CI_MERGE_REQUEST_IID - TOP_UPSTREAM_SOURCE_SHA - TOP_UPSTREAM_MERGE_REQUEST_IID - DEPLOY_VERSION - GITLAB_VERSION - GITLAB_SHELL_VERSION - GITLAB_ELASTICSEARCH_INDEXER_VERSION - GITLAB_KAS_VERSION - GITLAB_WORKHORSE_VERSION - GITLAB_PAGES_VERSION - GITALY_SERVER_VERSION - QA_IMAGE - QA_BROWSER - ].index_with { |val| ENV[val] }.compact_blank + lambda do + return {} unless Env.admin_personal_access_token || Env.personal_access_token + + client = Env.admin_personal_access_token ? API::Client.as_admin : API::Client.new + response = get(API::Request.new(client, '/version').url) + + JSON.parse(response.body, symbolize_names: true) + rescue StandardError, ArgumentError => e + Logger.error("Failed to attach version info to allure report: #{e}") + {} + end end end end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index 8a5e22fbc37..b5b572890c1 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -16,17 +16,21 @@ module QA enable_ip_limits if ip_limits end + # Personal access token + # + # It is possible to set the environment variable GITLAB_QA_ACCESS_TOKEN + # to use a specific access token rather than create one from the UI + # unless a specific user has been passed + # + # @return [String] def personal_access_token - @personal_access_token ||= begin - # you can set the environment variable GITLAB_QA_ACCESS_TOKEN - # to use a specific access token rather than create one from the UI - # unless a specific user has been passed - @user.nil? ? Runtime::Env.personal_access_token ||= create_personal_access_token : create_personal_access_token - end + @personal_access_token ||= if user.nil? + Runtime::Env.personal_access_token ||= create_personal_access_token + else + create_personal_access_token + end - if @user&.admin? - Runtime::Env.admin_personal_access_token = @personal_access_token - end + Runtime::Env.admin_personal_access_token = @personal_access_token if user&.admin? # rubocop:disable Cop/UserAdmin @personal_access_token end @@ -82,27 +86,38 @@ module QA Page::Main::Menu.perform(&:sign_out) end + # Create PAT + # + # Use api if admin personal access token is present and skip any UI actions otherwise perform creation via UI + # + # @return [String] def create_personal_access_token - signed_in_initially = Page::Main::Menu.perform(&:signed_in?) - - Page::Main::Menu.perform(&:sign_out) if @is_new_session && signed_in_initially - - token = Resource::PersonalAccessToken.fabricate! do |pat| - pat.user = user - end.token - - # If this is a new session, that tests that follow could fail if they - # try to sign in without starting a new session. - # Also, if the browser wasn't already signed in, leaving it - # signed in could cause tests to fail when they try to sign - # in again. For example, that would happen if a test has a - # before(:context) block that fabricates via the API, and - # it's the first test to run so it creates an access token - # - # Sign out so the tests can successfully sign in - Page::Main::Menu.perform(&:sign_out) if @is_new_session || !signed_in_initially - - token + if Runtime::Env.admin_personal_access_token + Resource::PersonalAccessToken.fabricate_via_api! do |pat| + pat.user = user + end.token + else + signed_in_initially = Page::Main::Menu.perform(&:signed_in?) + + Page::Main::Menu.perform(&:sign_out) if @is_new_session && signed_in_initially + + token = Resource::PersonalAccessToken.fabricate! do |pat| + pat.user = user + end.token + + # If this is a new session, that tests that follow could fail if they + # try to sign in without starting a new session. + # Also, if the browser wasn't already signed in, leaving it + # signed in could cause tests to fail when they try to sign + # in again. For example, that would happen if a test has a + # before(:context) block that fabricates via the API, and + # it's the first test to run so it creates an access token + # + # Sign out so the tests can successfully sign in + Page::Main::Menu.perform(&:sign_out) if @is_new_session || !signed_in_initially + + token + end end end end diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index 5adc52680f0..71e3383a534 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -46,6 +46,10 @@ module QA end end + def stop_primary_node + stop_node(@primary_node) + end + def start_primary_node start_node(@primary_node) end @@ -66,20 +70,29 @@ module QA start_node(@secondary_node) end + def stop_tertiary_node + stop_node(@tertiary_node) + end + + def start_tertiary_node + start_node(@tertiary_node) + end + def start_node(name) shell "docker start #{name}" + wait_until_shell_command_matches( + "docker inspect -f {{.State.Running}} #{name}", + /true/, + sleep_interval: 3, + max_duration: 180, + retry_on_exception: true + ) end def stop_node(name) shell "docker stop #{name}" end - def trigger_failover_by_stopping_primary_node - QA::Runtime::Logger.info("Stopping node #{@primary_node} to trigger failover") - stop_node(@primary_node) - wait_for_new_primary - end - def clear_replication_queue QA::Runtime::Logger.info("Clearing the replication queue") shell sql_to_docker_exec_cmd( @@ -157,22 +170,8 @@ module QA result[2].to_i end - # Makes the original primary (gitaly1) the primary again by - # stopping the other nodes, waiting for gitaly1 to be made the - # primary again, and then it starts the other nodes and enables - # writes - def reset_primary_to_original - QA::Runtime::Logger.info("Checking primary node...") - - return if @primary_node == current_primary_node - - QA::Runtime::Logger.info("Reset primary node to #{@primary_node}") + def start_all_nodes start_node(@primary_node) - stop_node(@secondary_node) - stop_node(@tertiary_node) - - wait_for_new_primary_node(@primary_node) - start_node(@secondary_node) start_node(@tertiary_node) @@ -189,10 +188,12 @@ module QA end def wait_for_praefect - QA::Runtime::Logger.info('Wait until Praefect starts and is listening') wait_until_shell_command_matches( - "docker exec #{@praefect} bash -c 'cat /var/log/gitlab/praefect/current'", - /listening at tcp address/ + "docker inspect -f {{.State.Running}} #{@praefect}", + /true/, + sleep_interval: 3, + max_duration: 180, + retry_on_exception: true ) # Praefect can fail to start if unable to dial one of the gitaly nodes @@ -204,20 +205,6 @@ module QA end end - def wait_for_new_primary_node(node) - QA::Runtime::Logger.info("Wait until #{node} is the primary node") - with_praefect_log(max_duration: 120) do |log| - break true if log['msg'] == 'primary node changed' && log['newPrimary'] == node - end - end - - def wait_for_new_primary - QA::Runtime::Logger.info("Wait until a new primary node is selected") - with_praefect_log(max_duration: 120) do |log| - break true if log['msg'] == 'primary node changed' - end - end - def wait_for_sql_ping wait_until_shell_command_matches( "docker exec #{@praefect} bash -c '/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml sql-ping'", @@ -274,10 +261,6 @@ module QA end end - def wait_for_health_check_current_primary_node - wait_for_health_check(current_primary_node) - end - def wait_for_health_check_all_nodes wait_for_health_check(@primary_node) wait_for_health_check(@secondary_node) @@ -286,29 +269,58 @@ module QA def wait_for_health_check(node) QA::Runtime::Logger.info("Waiting for health check on #{node}") - wait_until_shell_command("docker exec #{node} bash -c 'cat /var/log/gitlab/gitaly/current'") do |line| - QA::Runtime::Logger.debug(line.chomp) - log = JSON.parse(line) + wait_until_node_is_marked_as_healthy_storage(node) + end - log['grpc.request.fullMethod'] == '/grpc.health.v1.Health/Check' && log['grpc.code'] == 'OK' - rescue JSON::ParserError - # Ignore lines that can't be parsed as JSON - end + def wait_for_primary_node_health_check + wait_for_health_check(@primary_node) + end + + def wait_for_secondary_node_health_check + wait_for_health_check(@secondary_node) + end + + def wait_for_tertiary_node_health_check + wait_for_health_check(@tertiary_node) + end + + def wait_for_health_check_failure(node) + QA::Runtime::Logger.info("Waiting for health check failure on #{node}") + wait_until_node_is_removed_from_healthy_storages(node) + end + + def wait_for_primary_node_health_check_failure + wait_for_health_check_failure(@primary_node) end def wait_for_secondary_node_health_check_failure wait_for_health_check_failure(@secondary_node) end - def wait_for_health_check_failure(node) - QA::Runtime::Logger.info("Waiting for Praefect to record a health check failure on #{node}") - wait_until_shell_command("docker exec #{@praefect} bash -c 'tail -n 1 /var/log/gitlab/praefect/current'") do |line| - QA::Runtime::Logger.debug(line.chomp) - log = JSON.parse(line) + def wait_for_tertiary_node_health_check_failure + wait_for_health_check_failure(@tertiary_node) + end - health_check_failure_message?(log['msg']) && log['storage'] == node - rescue JSON::ParserError - # Ignore lines that can't be parsed as JSON + def wait_until_node_is_removed_from_healthy_storages(node) + Support::Waiter.wait_until(max_duration: 60, sleep_interval: 3, raise_on_failure: false) do + result = [] + shell sql_to_docker_exec_cmd("SELECT count(*) FROM healthy_storages WHERE storage = '#{node}';") do |line| + result << line + end + QA::Runtime::Logger.debug("result is ---#{result}") + result[2].to_i == 0 + end + end + + def wait_until_node_is_marked_as_healthy_storage(node) + Support::Waiter.wait_until(max_duration: 60, sleep_interval: 3, raise_on_failure: false) do + result = [] + shell sql_to_docker_exec_cmd("SELECT count(*) FROM healthy_storages WHERE storage = '#{node}';") do |line| + result << line + end + + QA::Runtime::Logger.debug("result is ---#{result}") + result[2].to_i == 1 end end diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb index 81cfaa125a9..5a35d8c251e 100644 --- a/qa/qa/service/shellout.rb +++ b/qa/qa/service/shellout.rb @@ -52,7 +52,7 @@ module QA end def wait_until_shell_command_matches(cmd, regex, **kwargs) - wait_until_shell_command(cmd, kwargs) do |line| + wait_until_shell_command(cmd, **kwargs) do |line| QA::Runtime::Logger.debug(line.chomp) line =~ regex 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 1422dd5a029..8b4900957c5 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 @@ -89,7 +89,7 @@ module QA end end - context 'with milestones' do + context 'with milestones and badges' do let(:source_milestone) do Resource::GroupMilestone.fabricate_via_api! do |milestone| milestone.api_client = api_client @@ -99,10 +99,17 @@ module QA before do source_milestone + + Resource::GroupBadge.fabricate_via_api! do |badge| + badge.api_client = api_client + badge.group = source_group + badge.link_url = "http://example.com/badge" + badge.image_url = "http://shields.io/badge" + end end it( - 'successfully imports group milestones', + 'successfully imports group milestones and badges', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2245' ) do expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration) @@ -113,8 +120,40 @@ module QA expect(imported_milestone.iid).to eq(source_milestone.iid) expect(imported_milestone.created_at).to eq(source_milestone.created_at) expect(imported_milestone.updated_at).to eq(source_milestone.updated_at) + + expect(imported_group.badges).to eq(source_group.badges) + end + end + end + + context 'with group members' do + let(:member) do + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + usr.hard_delete_on_api_removal = true end end + + before do + member.set_public_email + source_group.add_member(member, Resource::Members::AccessLevel::DEVELOPER) + end + + after do + member.remove_via_api! + end + + it( + 'adds members for imported group', + testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/2310' + ) do + expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration) + + imported_member = imported_group.reload!.members.find { |usr| usr.username == member.username } + + expect(imported_member).not_to be_nil + expect(imported_member.access_level).to eq(Resource::Members::AccessLevel::DEVELOPER) + end end after do 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 new file mode 100644 index 00000000000..9935908d55e --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/bulk_import_project_spec.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage', :requires_admin 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 + 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(:sandbox) do + Resource::Sandbox.fabricate_via_api! do |group| + group.api_client = admin_api_client + end + end + + let(:source_group) do + Resource::Sandbox.fabricate_via_api! do |group| + group.api_client = api_client + group.path = "source-group-for-import-#{SecureRandom.hex(4)}" + end + end + + let(:source_project) do + Resource::Project.fabricate_via_api! do |project| + project.api_client = api_client + project.group = source_group + end + end + + let(:imported_group) do + Resource::BulkImportGroup.fabricate_via_api! do |group| + group.api_client = api_client + group.sandbox = sandbox + group.source_group_path = source_group.path + end + 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 + 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 + 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) + + imported_projects = imported_group.reload!.projects + aggregate_failures do + expect(imported_projects.count).to eq(1) + expect(imported_projects.first).to eq(source_project) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb index b51a79f239c..3a2f960d812 100644 --- a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb @@ -9,11 +9,6 @@ module QA let(:differ) { RSpec::Support::Differ.new(color: true) } let(:api_client) { Runtime::API::Client.as_admin } - let(:group) do - Resource::Group.fabricate_via_api! do |resource| - resource.api_client = api_client - end - end let(:user) do Resource::User.fabricate_via_api! do |resource| @@ -86,19 +81,15 @@ module QA Resource::ProjectImportedFromGithub.fabricate_via_api! do |project| project.add_name_uuid = false project.name = 'imported-project' - project.group = group project.github_personal_access_token = Runtime::Env.github_access_token project.github_repository_path = github_repo + project.personal_namespace = user.username project.api_client = api_client end end - before do - group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) - end - after do |example| - user.remove_via_api! + user.remove_via_api! unless example.exception next unless defined?(@import_time) # save data for comparison after run finished @@ -128,7 +119,10 @@ module QA ) end - it 'imports large Github repo via api', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1880' do + it( + 'imports large Github repo via api', + testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1880' + ) do start = Time.now Runtime::Logger.info("Importing project '#{imported_project.full_path}'") # import the project and log path 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 19fdb37f788..ec4f0387128 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 @@ -14,7 +14,7 @@ module QA before(:context) do # Reset the cluster in case previous tests left it in a bad state - praefect_manager.reset_primary_to_original + praefect_manager.start_all_nodes project = Resource::Project.fabricate! do |project| project.name = "gitaly_cluster" @@ -25,25 +25,35 @@ module QA after(:context, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238187', type: :stale }) do # Leave the cluster in a suitable state for subsequent tests, # if there was a problem during the tests here - praefect_manager.reset_primary_to_original + praefect_manager.start_all_nodes end it 'automatically fails over', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1267' do # Create a new project with a commit and wait for it to replicate + + # make sure that our project is published to the 'primary' node + praefect_manager.stop_secondary_node + praefect_manager.stop_tertiary_node + praefect_manager.wait_for_secondary_node_health_check_failure + praefect_manager.wait_for_tertiary_node_health_check_failure + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.commit_message = first_added_commit_message push.new_branch = false - push.file_content = "This should exist on both nodes" + push.file_content = "This should exist on all nodes" end + praefect_manager.start_secondary_node + praefect_manager.start_tertiary_node + praefect_manager.wait_for_health_check_all_nodes + praefect_manager.wait_for_replication(project.id) # Stop the primary node to trigger failover, and then wait # for Gitaly to be ready for writes again - praefect_manager.trigger_failover_by_stopping_primary_node - praefect_manager.wait_for_new_primary - praefect_manager.wait_for_health_check_current_primary_node + praefect_manager.stop_primary_node + praefect_manager.wait_for_primary_node_health_check_failure praefect_manager.wait_for_gitaly_check Resource::Repository::Commit.fabricate_via_api! do |commit| @@ -69,7 +79,7 @@ module QA it 'automatically reconciles', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238187', type: :stale }, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1266' do # Start the old primary node again praefect_manager.start_primary_node - praefect_manager.wait_for_health_check_current_primary_node + praefect_manager.wait_for_primary_node_health_check # Confirm automatic reconciliation expect(praefect_manager.replicated?(project.id)).to be true @@ -81,7 +91,7 @@ module QA .and include(second_added_commit_message) # Restore the original primary node - praefect_manager.reset_primary_to_original + praefect_manager.start_all_nodes # Check that all commits are still available even though the primary # node was offline when one was made 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 f00321ee3f5..37670b70fd8 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 @@ -14,12 +14,12 @@ module QA before do # Reset the cluster in case previous tests left it in a bad state - praefect_manager.reset_primary_to_original + praefect_manager.start_all_nodes end after do # Leave the cluster in a suitable state for subsequent tests - praefect_manager.reset_primary_to_original + praefect_manager.start_all_nodes end it 'recovers from dataloss', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1265' do @@ -28,9 +28,7 @@ module QA # Stop the primary node to trigger failover, and then wait # for Gitaly to be ready for writes again - praefect_manager.trigger_failover_by_stopping_primary_node - praefect_manager.wait_for_new_primary - praefect_manager.wait_for_health_check_current_primary_node + praefect_manager.stop_primary_node praefect_manager.wait_for_gitaly_check # Confirm that we have access to the repo after failover diff --git a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb index caaa615149d..db75d4639ff 100644 --- a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb @@ -46,10 +46,9 @@ module QA def create_project(user, api_client, project_name) project = Resource::Project.fabricate_via_api! do |project| - project.personal_namespace = true + project.personal_namespace = user.username project.add_name_uuid = false project.name = project_name - project.path_with_namespace = "#{user.username}/#{project_name}" project.api_client = api_client 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 c3be58fda74..c136d14c1e5 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 @@ -58,8 +58,8 @@ module QA testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1806', issue_1: 'https://gitlab.com/gitlab-org/gitlab/-/issues/331252', issue_2: 'https://gitlab.com/gitlab-org/gitlab/-/issues/333678', - # mostly impacts testing as it makes small groups import slower - issue_3: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332351' + issue_3: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332351', + except: { job: 'instance-image-slow-network' } ) do Page::Group::BulkImport.perform do |import_page| import_page.import_group(imported_group.path, imported_group.sandbox.path) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb index a3235543998..4e15aa79623 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb @@ -78,6 +78,7 @@ module QA @otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content) two_fa_auth.set_pin_code(@otp.fresh_otp) + two_fa_auth.set_current_password(user.password) two_fa_auth.click_register_2fa_app_button recovery_code = two_fa_auth.recovery_codes.sample diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb index 8a2bbc92eca..b97da194795 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb @@ -55,6 +55,7 @@ module QA Page::Profile::TwoFactorAuth.perform do |two_fa_auth| otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content) two_fa_auth.set_pin_code(otp.fresh_otp) + two_fa_auth.set_current_password(user.password) two_fa_auth.click_register_2fa_app_button two_fa_auth.click_copy_and_proceed end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb index 2fe1cbabee3..78fbec594a3 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb @@ -98,6 +98,7 @@ module QA @otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content) two_fa_auth.set_pin_code(@otp.fresh_otp) + two_fa_auth.set_current_password(user.password) two_fa_auth.click_register_2fa_app_button two_fa_auth.click_copy_and_proceed 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 974d9b02f4d..af4e7126c29 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 @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage', :smoke do - describe 'Project' do + describe 'Project', :requires_admin do shared_examples 'successful project creation' do it 'creates a new project' do Page::Project::Show.perform do |project| @@ -17,6 +17,7 @@ module QA end before do + Runtime::Feature.enable(:paginatable_namespace_drop_down_for_project_creation) Flow::Login.sign_in project end @@ -39,7 +40,7 @@ module QA Resource::Project.fabricate_via_browser_ui! do |project| project.name = project_name project.description = 'create awesome project test' - project.personal_namespace = true + project.personal_namespace = Runtime::User.username end end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 4090837d5c9..f2d4fc6e677 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -1,22 +1,29 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/332588', type: :investigating } do + RSpec.describe 'Create' do describe 'Merge request creation from fork' do - # TODO: Please add this back to :smoke suite as soon as https://gitlab.com/gitlab-org/gitlab/-/issues/332588 is addressed - it 'can merge feature branch fork to mainline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1305' do - merge_request = Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request| + let(:merge_request) do + Resource::MergeRequestFromFork.fabricate_via_browser_ui! do |merge_request| merge_request.fork_branch = 'feature-branch' end + end + + before do + Flow::Login.sign_in + end - Flow::Login.while_signed_in do - merge_request.visit! + after do + merge_request.fork.remove_via_api! + end + + it 'can merge feature branch fork to mainline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1305' do + merge_request.visit! - Page::MergeRequest::Show.perform do |merge_request| - merge_request.merge! + Page::MergeRequest::Show.perform do |merge_request| + merge_request.merge! - expect(merge_request).to have_content('The changes were merged') - end + expect(merge_request).to be_merged end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb index 0ea294b8e51..6a79a2d8078 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', quarantine: { - only: { job: 'large-setup' }, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338324', - type: :stale - } do + RSpec.describe 'Create' do describe 'Merged merge request' do let(:project) do Resource::Project.fabricate_via_api! do |project| @@ -23,7 +19,7 @@ module QA Flow::Login.sign_in end - it 'can be reverted', :can_use_large_setup, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1818' do + it 'can be reverted', :can_use_large_setup, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1818', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/335987', type: :investigating } do revertable_merge_request.visit! Page::MergeRequest::Show.perform do |merge_request| 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 5cebbb32ade..1752513a831 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 @@ -46,11 +46,11 @@ module QA merge_request.visit! end - it 'applies multiple suggestions', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1838' do + 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 Page::MergeRequest::Show.perform do |merge_request| merge_request.click_diffs_tab 4.times { merge_request.add_suggestion_to_batch } - merge_request.apply_suggestions_batch + merge_request.apply_suggestion_with_message("Custom commit message") expect(merge_request).to have_css('.badge-success', text: "Applied", count: 4) end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb index 1080d8ab849..300fd6a1be2 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb @@ -3,13 +3,7 @@ module QA RSpec.describe 'Create' do # convert back to a smoke test once proved to be stable describe 'Personal snippet creation' do - it 'user creates a personal snippet', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1357' do - Flow::Login.sign_in - - Page::Main::Menu.perform do |menu| - menu.go_to_menu_dropdown_option(:snippets_link) - end - + let(:snippet) do Resource::Snippet.fabricate_via_browser_ui! do |snippet| snippet.title = 'Snippet title' snippet.description = 'Snippet description' @@ -17,6 +11,18 @@ module QA snippet.file_name = 'ruby_file.rb' snippet.file_content = 'File.read("test.txt").split(/\n/)' end + end + + before do + Flow::Login.sign_in + end + + after do + snippet.remove_via_api! + end + + it 'user creates a personal snippet', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1357' do + snippet.visit! Page::Dashboard::Snippet::Show.perform do |snippet| expect(snippet).to have_snippet_title('Snippet title') diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb index 525fc5799a9..a32ee472150 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb @@ -3,17 +3,11 @@ module QA RSpec.describe 'Create' do describe 'Multiple file snippet' do - it 'creates a personal snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1654' do - Flow::Login.sign_in - - Page::Main::Menu.perform do |menu| - menu.go_to_menu_dropdown_option(:snippets_link) - end - + let(:snippet) do Resource::Snippet.fabricate_via_browser_ui! do |snippet| snippet.title = 'Personal snippet with multiple files' snippet.description = 'Snippet description' - snippet.visibility = 'Public' + snippet.visibility = 'Private' snippet.file_name = 'First file name' snippet.file_content = 'First file content' @@ -22,11 +16,23 @@ module QA files.append(name: 'Third file name', content: 'Third file content') end end + end + + before do + Flow::Login.sign_in + end + + after do + snippet.remove_via_api! + end + + it 'creates a personal snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1654' do + snippet.visit! Page::Dashboard::Snippet::Show.perform do |snippet| expect(snippet).to have_snippet_title('Personal snippet with multiple files') expect(snippet).to have_snippet_description('Snippet description') - expect(snippet).to have_visibility_type(/public/i) + expect(snippet).to have_visibility_type(/private/i) expect(snippet).to have_file_name('First file name', 1) expect(snippet).to have_file_content('First file content', 1) expect(snippet).to have_file_name('Second file name', 2) diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb index e4c2488e8df..1967273ca17 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb @@ -3,9 +3,7 @@ module QA RSpec.describe 'Create' do # to be converted to a smoke test once proved to be stable describe 'Project snippet creation' do - it 'user creates a project snippet', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1358' do - Flow::Login.sign_in - + let(:snippet) do Resource::ProjectSnippet.fabricate_via_browser_ui! do |snippet| snippet.title = 'Project snippet' snippet.description = ' ' @@ -13,6 +11,18 @@ module QA snippet.file_name = 'markdown_file.md' snippet.file_content = "### Snippet heading\n\n[Gitlab link](https://gitlab.com/)" end + end + + before do + Flow::Login.sign_in + end + + after do + snippet.remove_via_api! + end + + it 'user creates a project snippet', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1358' do + snippet.visit! Page::Dashboard::Snippet::Show.perform do |snippet| expect(snippet).to have_snippet_title('Project snippet') diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb index 3298989cc12..ae71be26a38 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb @@ -3,9 +3,7 @@ module QA RSpec.describe 'Create' do describe 'Multiple file snippet' do - it 'creates a project snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1648' do - Flow::Login.sign_in - + let(:snippet) do Resource::ProjectSnippet.fabricate_via_browser_ui! do |snippet| snippet.title = 'Project snippet with multiple files' snippet.description = 'Snippet description' @@ -20,6 +18,18 @@ module QA end end end + end + + before do + Flow::Login.sign_in + end + + after do + snippet.remove_via_api! + end + + it 'creates a project snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1648' do + snippet.visit! Page::Dashboard::Snippet::Show.perform do |snippet| aggregate_failures 'file content verification' do diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb new file mode 100644 index 00000000000..51791c01048 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module QA + RSpec.describe( + 'Create', + :runner, + quarantine: { + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338179', + type: :bug + } + ) do + describe 'Web IDE web terminal' do + before do + project = Resource::Project.fabricate_via_api! do |project| + project.name = 'web-terminal-project' + end + + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab/.gitlab-webide.yml' + commit.add_files( + [ + { + file_path: '.gitlab/.gitlab-webide.yml', + content: <<~YAML + terminal: + tags: ["web-ide"] + script: sleep 60 + YAML + } + ] + ) + end + + @runner = Resource::Runner.fabricate_via_api! do |runner| + runner.project = project + runner.name = "qa-runner-#{Time.now.to_i}" + runner.tags = %w[web-ide] + runner.image = 'gitlab/gitlab-runner:latest' + runner.config = <<~END + concurrent = 1 + + [session_server] + listen_address = "0.0.0.0:8093" + advertise_address = "localhost:8093" + session_timeout = 120 + END + end + + Flow::Login.sign_in + + project.visit! + end + + after do + @runner.remove_via_api! if @runner + end + + it 'user starts the web terminal', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/quality/test_cases/1593' do + Page::Project::Show.perform(&:open_web_ide!) + + # Start the web terminal and check that there were no errors + # The terminal screen is a canvas element, so we can't read its content, + # so we infer that it's working if: + # a) The terminal JS package has loaded, and + # b) It's not stuck in a "Loading/Starting" state, and + # c) There's no alert stating there was a problem + # + # The terminal itself is a third-party package so we assume it is + # adequately tested elsewhere. + # + # There are also FE specs + # * spec/frontend/ide/components/terminal/terminal_controls_spec.js + Page::Project::WebIDE::Edit.perform do |edit| + edit.start_web_terminal + + expect(edit).to have_no_alert + expect(edit).to have_finished_loading + expect(edit).to have_terminal_screen + 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_spec.rb index bfcc49885a0..ea7f7cc1c05 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_spec.rb @@ -28,9 +28,9 @@ module QA project.group.visit! - Page::Group::Menu.perform(&:go_to_dependency_proxy) + Page::Group::Menu.perform(&:go_to_package_settings) - Page::Group::DependencyProxy.perform do |index| + Page::Group::Settings::PackageRegistries.perform do |index| expect(index).to have_dependency_proxy_enabled end end diff --git a/qa/qa/specs/features/browser_ui/5_package/helm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/helm_registry_spec.rb new file mode 100644 index 00000000000..fe52fd03ad8 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/5_package/helm_registry_spec.rb @@ -0,0 +1,150 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Package', :orchestrated, :packages, :object_storage do + describe 'Helm Registry' do + include Runtime::Fixtures + include_context 'packages registry qa scenario' + + let(:package_name) { 'gitlab_qa_helm' } + let(:package_version) { '1.3.7' } + let(:package_type) { 'helm' } + + let(:package_gitlab_ci_file) do + { + file_path: '.gitlab-ci.yml', + content: + <<~YAML + deploy: + image: alpine:3 + script: + - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing + - apk add curl + - helm create #{package_name} + - cp ./Chart.yaml #{package_name} + - helm package #{package_name} + - http_code=$(curl --write-out "%{http_code}" --request POST --form 'chart=@#{package_name}-#{package_version}.tgz' --user #{username}:#{access_token} ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/api/stable/charts --output /dev/null --silent) + - '[ $http_code = "201" ]' + only: + - "#{package_project.default_branch}" + tags: + - "runner-for-#{package_project.group.name}" + YAML + } + end + + let(:package_chart_yaml_file) do + { + file_path: "Chart.yaml", + content: + <<~EOF + apiVersion: v2 + name: #{package_name} + description: GitLab QA helm package + type: application + version: #{package_version} + appVersion: "1.16.0" + EOF + } + end + + let(:client_gitlab_ci_file) do + { + file_path: '.gitlab-ci.yml', + content: + <<~YAML + pull: + image: alpine:3 + script: + - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing + - helm repo add --username #{username} --password #{access_token} gitlab_qa ${CI_API_V4_URL}/projects/#{package_project.id}/packages/helm/stable + - helm repo update + - helm pull gitlab_qa/#{package_name} + only: + - "#{client_project.default_branch}" + tags: + - "runner-for-#{client_project.group.name}" + YAML + } + end + + %i[personal_access_token ci_job_token project_deploy_token].each do |authentication_token_type| + context "using a #{authentication_token_type}" do + let(:username) do + case authentication_token_type + when :personal_access_token + Runtime::User.username + when :ci_job_token + 'gitlab-ci-token' + when :project_deploy_token + project_deploy_token.username + end + end + + let(:access_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 "pushes and pulls a helm chart" 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_chart_yaml_file]) + 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]) + end + + client_project.visit! + + Flow::Pipeline.visit_latest_pipeline + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('pull') + end + + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 800) + end + end + end + end + end + end +end 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/maven_gradle_repository_spec.rb index fb3f2abd87a..ec9feca84b9 100644 --- a/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/maven_gradle_repository_spec.rb @@ -5,57 +5,13 @@ module QA describe 'Maven Repository with Gradle' do using RSpec::Parameterized::TableSyntax include Runtime::Fixtures + include_context 'packages registry qa scenario' let(:group_id) { 'com.gitlab.qa' } let(:artifact_id) { 'maven_gradle' } let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') } let(:package_version) { '1.3.7' } - - let(:personal_access_token) { Runtime::Env.personal_access_token } - - let(:package_project) do - Resource::Project.fabricate_via_api! do |project| - project.name = 'maven-with-gradle-project' - project.initialize_with_readme = true - project.visibility = :private - end - end - - let(:client_project) do - Resource::Project.fabricate_via_api! do |client_project| - client_project.name = 'gradle_client' - client_project.initialize_with_readme = true - client_project.group = package_project.group - end - end - - let(:package) do - Resource::Package.init do |package| - package.name = package_name - package.project = package_project - end - end - - let(:runner) do - Resource::Runner.fabricate! do |runner| - runner.name = "qa-runner-#{Time.now.to_i}" - runner.tags = ["runner-for-#{package_project.group.name}"] - runner.executor = :docker - runner.token = package_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(:project_deploy_token) do - Resource::DeployToken.fabricate_via_browser_ui! do |deploy_token| - deploy_token.name = 'maven-with-gradle-deploy-token' - deploy_token.project = package_project - end - end + let(:package_type) { 'maven_gradle' } let(:package_gitlab_ci_file) do { @@ -127,18 +83,6 @@ module QA } end - before do - Flow::Login.sign_in_unless_signed_in - runner - end - - after do - runner.remove_via_api! - package.remove_via_api! - package_project.remove_via_api! - client_project.remove_via_api! - end - where(:authentication_token_type, :maven_header_name) do :personal_access_token | 'Private-Token' :ci_job_token | 'Job-Token' diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb index ba41285ebca..b928eae62e6 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Configure', except: { job: 'review-qa-*' } do - describe 'Kubernetes Cluster Integration', :requires_admin, :skip_live_env, :smoke do + describe 'Kubernetes Cluster Integration', :orchestrated, :requires_admin, :skip_live_env do context 'Project Clusters' do let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! } let(:project) do diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 205ddf7ad3a..663761805ee 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -8,63 +8,93 @@ module QA HTTP_STATUS_NO_CONTENT = 204 HTTP_STATUS_ACCEPTED = 202 HTTP_STATUS_NOT_FOUND = 404 + HTTP_STATUS_TOO_MANY_REQUESTS = 429 HTTP_STATUS_SERVER_ERROR = 500 def post(url, payload, args = {}) - default_args = { - method: :post, - url: url, - payload: payload, - verify_ssl: false - } - - RestClient::Request.execute( - default_args.merge(args) - ) - rescue RestClient::ExceptionWithResponse => e - return_response_or_raise(e) + with_retry_on_too_many_requests do + default_args = { + method: :post, + url: url, + payload: payload, + verify_ssl: false + } + + RestClient::Request.execute( + default_args.merge(args) + ) + rescue RestClient::ExceptionWithResponse => e + return_response_or_raise(e) + end end def get(url, args = {}) - default_args = { - method: :get, - url: url, - verify_ssl: false - } - - RestClient::Request.execute( - default_args.merge(args) - ) - rescue RestClient::ExceptionWithResponse => e - return_response_or_raise(e) + with_retry_on_too_many_requests do + default_args = { + method: :get, + url: url, + verify_ssl: false + } + + RestClient::Request.execute( + default_args.merge(args) + ) + rescue RestClient::ExceptionWithResponse => e + return_response_or_raise(e) + end end def put(url, payload = nil) - RestClient::Request.execute( - method: :put, - url: url, - payload: payload, - verify_ssl: false) - rescue RestClient::ExceptionWithResponse => e - return_response_or_raise(e) + with_retry_on_too_many_requests do + RestClient::Request.execute( + method: :put, + url: url, + payload: payload, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + return_response_or_raise(e) + end end def delete(url) - RestClient::Request.execute( - method: :delete, - url: url, - verify_ssl: false) - rescue RestClient::ExceptionWithResponse => e - return_response_or_raise(e) + with_retry_on_too_many_requests do + RestClient::Request.execute( + method: :delete, + url: url, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + return_response_or_raise(e) + end end def head(url) - RestClient::Request.execute( - method: :head, - url: url, - verify_ssl: false) - rescue RestClient::ExceptionWithResponse => e - return_response_or_raise(e) + with_retry_on_too_many_requests do + RestClient::Request.execute( + method: :head, + url: url, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + return_response_or_raise(e) + end + end + + def with_retry_on_too_many_requests + response = nil + + Support::Retrier.retry_until(log: false) do + response = yield + + if response.code == HTTP_STATUS_TOO_MANY_REQUESTS + wait_seconds = response.headers[:retry_after].to_i + QA::Runtime::Logger.debug("Received 429 - Too many requests. Waiting for #{wait_seconds} seconds.") + + sleep wait_seconds + end + + response.code != HTTP_STATUS_TOO_MANY_REQUESTS + end + + response end def parse_body(response) diff --git a/qa/qa/support/formatters/test_stats_formatter.rb b/qa/qa/support/formatters/test_stats_formatter.rb index 0f76a924b10..0484bd7f90f 100644 --- a/qa/qa/support/formatters/test_stats_formatter.rb +++ b/qa/qa/support/formatters/test_stats_formatter.rb @@ -32,7 +32,6 @@ module QA influxdb_token, bucket: 'e2e-test-stats', org: 'gitlab-qa', - use_ssl: false, precision: InfluxDB2::WritePrecision::NANOSECOND ) end @@ -57,19 +56,22 @@ module QA # @param [RSpec::Core::Example] example # @return [Hash] def test_stats(example) + file_path = example.metadata[:file_path].gsub('./qa/specs/features', '') + { name: 'test-stats', time: time, tags: { name: example.full_description, - file_path: example.metadata[:file_path].gsub('./qa/specs/features', ''), + file_path: file_path, status: example.execution_result.status, reliable: example.metadata.key?(:reliable).to_s, quarantined: example.metadata.key?(:quarantine).to_s, retried: ((example.metadata[:retry_attempts] || 0) > 0).to_s, job_name: job_name, merge_request: merge_request, - run_type: env('QA_RUN_TYPE') || run_type + run_type: env('QA_RUN_TYPE') || run_type, + stage: devops_stage(file_path) }, fields: { id: example.id, @@ -113,11 +115,11 @@ module QA @merge_request ||= (!!env('CI_MERGE_REQUEST_IID') || !!env('TOP_UPSTREAM_MERGE_REQUEST_IID')).to_s end - # Test run type from staging, canary or production env + # Test run type from staging, canary, preprod or production env # - # @return [String>, nil] + # @return [String, nil] def run_type - return unless %w[staging canary production].include?(project_name) + return unless %w[staging canary preprod production].include?(project_name) @run_type ||= begin test_subset = if env('NO_ADMIN') == 'true' @@ -150,6 +152,14 @@ module QA ENV[name] end + + # Get spec devops stage + # + # @param [String] location + # @return [String, nil] + def devops_stage(file_path) + file_path.match(%r{(\d{1,2}_\w+)/})&.captures&.first + end end end end diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb index fde8ac263ca..aa568d633fc 100644 --- a/qa/qa/support/retrier.rb +++ b/qa/qa/support/retrier.rb @@ -34,15 +34,29 @@ module QA result end - def retry_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true) + def retry_until( + max_attempts: nil, + max_duration: nil, + reload_page: nil, + sleep_interval: 0, + raise_on_failure: true, + retry_on_exception: false, + log: true + ) # For backwards-compatibility max_attempts = 3 if max_attempts.nil? && max_duration.nil? if log - start_msg ||= ["with retry_until:"] + 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 << "reload_page: #{reload_page}; sleep_interval: #{sleep_interval}; raise_on_failure: #{raise_on_failure}; retry_on_exception: #{retry_on_exception}" + 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 @@ -58,7 +72,7 @@ module QA ) do result = yield end - QA::Runtime::Logger.debug("ended retry_until") + QA::Runtime::Logger.debug("ended retry_until") if log result end diff --git a/qa/qa/tools/delete_subgroups.rb b/qa/qa/tools/delete_subgroups.rb index 2734a702536..bc905fdeadd 100644 --- a/qa/qa/tools/delete_subgroups.rb +++ b/qa/qa/tools/delete_subgroups.rb @@ -20,16 +20,10 @@ module QA end def run - $stdout.puts 'Running...' + $stdout.puts 'Fetching subgroups for deletion...' - # Fetch group's id - group_id = fetch_group_id - - sub_groups_head_response = head Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", per_page: "100").url - total_sub_group_pages = sub_groups_head_response.headers[:x_total_pages] - - sub_group_ids = fetch_subgroup_ids(group_id, total_sub_group_pages) - $stdout.puts "Number of Sub Groups not already marked for deletion: #{sub_group_ids.length}" + sub_group_ids = fetch_subgroup_ids + $stdout.puts "\nNumber of Sub Groups not already marked for deletion: #{sub_group_ids.length}" delete_subgroups(sub_group_ids) unless sub_group_ids.empty? $stdout.puts "\nDone" @@ -52,12 +46,20 @@ module QA JSON.parse(group_search_response.body)["id"] end - def fetch_subgroup_ids(group_id, group_pages) + def fetch_subgroup_ids + group_id = fetch_group_id sub_groups_ids = [] + page_no = '1' + + # When we reach the last page, the x-next-page header is a blank string + while page_no.present? + $stdout.print '.' + + sub_groups_response = get Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", page: page_no, per_page: '100').url + sub_groups_ids.concat(JSON.parse(sub_groups_response.body) + .reject { |subgroup| !subgroup["marked_for_deletion_on"].nil? }.map { |subgroup| subgroup['id'] }) - group_pages.to_i.times do |page_no| - sub_groups_response = get Runtime::API::Request.new(@api_client, "/groups/#{group_id}/subgroups", page: (page_no + 1).to_s, per_page: "100").url - sub_groups_ids.concat(JSON.parse(sub_groups_response.body).reject { |subgroup| !subgroup["marked_for_deletion_on"].nil? }.map { |subgroup| subgroup["id"] }) + page_no = sub_groups_response.headers[:x_next_page].to_s end sub_groups_ids.uniq diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 4f0f93bf020..e25892a008f 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -17,6 +17,7 @@ QA::Runtime::AllureReport.configure! QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| require f } +Dir[::File.join(__dir__, "support/shared_contexts/*.rb")].sort.each { |f| require f } RSpec.configure do |config| config.include QA::Support::Matchers::EventuallyMatcher diff --git a/qa/spec/specs/allure_report_spec.rb b/qa/spec/specs/allure_report_spec.rb index 34116ca6cbd..03bf77039cc 100644 --- a/qa/spec/specs/allure_report_spec.rb +++ b/qa/spec/specs/allure_report_spec.rb @@ -45,14 +45,18 @@ describe QA::Runtime::AllureReport do let(:png_file) { 'png-file' } let(:html_file) { 'html-file' } let(:ci_job) { 'ee:relative 5' } + let(:versions) { { version: '14', revision: '6ced31db947' } } before do stub_env('CI', 'true') stub_env('CI_JOB_NAME', ci_job) + stub_env('GITLAB_QA_ADMIN_ACCESS_TOKEN', 'token') allow(Allure).to receive(:add_attachment) allow(File).to receive(:open).with(png_path) { png_file } allow(File).to receive(:open).with(html_path) { html_file } + 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') described_class.configure! end @@ -61,7 +65,7 @@ describe QA::Runtime::AllureReport do aggregate_failures do expect(allure_config.results_directory).to eq('tmp/allure-results') expect(allure_config.clean_results_directory).to eq(true) - expect(allure_config.environment_properties).to be_a_kind_of(Hash) + expect(allure_config.environment_properties.call).to eq(versions) expect(allure_config.environment).to eq('ee:relative') end end diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_stats_formatter_spec.rb index fec7ec1c7c0..859d45a660b 100644 --- a/qa/spec/support/formatters/test_stats_formatter_spec.rb +++ b/qa/spec/support/formatters/test_stats_formatter_spec.rb @@ -18,12 +18,13 @@ describe QA::Support::Formatters::TestStatsFormatter do let(:quarantined) { 'false' } let(:influx_client) { instance_double('InfluxDB2::Client', create_write_api: influx_write_api) } 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(:influx_client_args) do { bucket: 'e2e-test-stats', org: 'gitlab-qa', - use_ssl: false, precision: InfluxDB2::WritePrecision::NANOSECOND } end @@ -34,14 +35,15 @@ describe QA::Support::Formatters::TestStatsFormatter do time: DateTime.strptime(ci_timestamp).to_time, tags: { name: 'stats export spec', - file_path: './spec/support/formatters/test_stats_formatter_spec.rb', + file_path: file_path.gsub('./qa/specs/features', ''), status: :passed, reliable: reliable, quarantined: quarantined, retried: "false", job_name: "test-job", merge_request: "false", - run_type: run_type + run_type: run_type, + stage: stage }, fields: { id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]', @@ -57,7 +59,9 @@ describe QA::Support::Formatters::TestStatsFormatter do def run_spec(&spec) spec ||= -> { it('spec') {} } - describe_successfully('stats export', &spec) + describe_successfully('stats export', &spec).tap do |example_group| + example_group.examples.each { |ex| ex.metadata[:file_path] = file_path } + end send_stop_notification 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 new file mode 100644 index 00000000000..6e197015640 --- /dev/null +++ b/qa/spec/support/shared_contexts/packages_registry_shared_context.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module QA + RSpec.shared_context 'packages registry qa scenario' do + let(:personal_access_token) { Runtime::Env.personal_access_token } + + let(:package_project) do + Resource::Project.fabricate_via_api! do |project| + project.name = "#{package_type}_package_project" + project.initialize_with_readme = true + project.visibility = :private + end + end + + let(:client_project) do + Resource::Project.fabricate_via_api! do |client_project| + client_project.name = "#{package_type}_client_project" + client_project.initialize_with_readme = true + client_project.group = package_project.group + end + end + + let(:package) do + Resource::Package.init do |package| + package.name = package_name + package.project = package_project + end + end + + let(:runner) do + Resource::Runner.fabricate! do |runner| + runner.name = "qa-runner-#{Time.now.to_i}" + runner.tags = ["runner-for-#{package_project.group.name}"] + runner.executor = :docker + runner.token = package_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(:project_deploy_token) do + Resource::DeployToken.fabricate_via_browser_ui! do |deploy_token| + deploy_token.name = 'helm-package-deploy-token' + deploy_token.project = package_project + end + end + + before do + Flow::Login.sign_in_unless_signed_in + runner + end + + after do + runner.remove_via_api! + package.remove_via_api! + package_project.remove_via_api! + client_project.remove_via_api! + end + end +end |