diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 09:08:42 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2021-08-19 09:08:42 +0000 |
commit | b76ae638462ab0f673e5915986070518dd3f9ad3 (patch) | |
tree | bdab0533383b52873be0ec0eb4d3c66598ff8b91 /qa | |
parent | 434373eabe7b4be9593d18a585fb763f1e5f1a6f (diff) | |
download | gitlab-ce-b76ae638462ab0f673e5915986070518dd3f9ad3.tar.gz |
Add latest changes from gitlab-org/gitlab@14-2-stable-eev14.2.0-rc42
Diffstat (limited to 'qa')
87 files changed, 2738 insertions, 574 deletions
diff --git a/qa/Gemfile b/qa/Gemfile index ff2074b6191..3ba1244d17e 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -5,14 +5,14 @@ source 'https://rubygems.org' gem 'gitlab-qa' gem 'activesupport', '~> 6.1.3.2' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.14.1' -gem 'capybara', '~> 3.29.0' +gem 'capybara', '~> 3.35.0' gem 'capybara-screenshot', '~> 1.0.23' gem 'rake', '~> 12.3.3' -gem 'rspec', '~> 3.7' +gem 'rspec', '~> 3.10' gem 'selenium-webdriver', '~> 4.0.0.beta4' gem 'airborne', '~> 0.3.4' gem 'rest-client', '~> 2.1.0' -gem 'nokogiri', '~> 1.11.1' +gem 'nokogiri', '~> 1.11.7' gem 'rspec-retry', '~> 0.6.1' gem 'rspec_junit_formatter', '~> 0.4.1' gem 'faker', '~> 1.6', '>= 1.6.6' @@ -22,12 +22,14 @@ gem 'rotp', '~> 3.1.0' gem 'timecop', '~> 0.9.1' gem 'parallel', '~> 1.19' gem 'rspec-parameterized', '~> 0.4.2' -gem "octokit", "~> 4.21" -gem "webdrivers", "~> 4.6" +gem 'octokit', '~> 4.21' +gem 'webdrivers', '~> 4.6' gem 'chemlab', '~> 0.7' gem 'chemlab-library-www-gitlab-com', '~> 0.1' +gem 'deprecation_toolkit', '~> 1.5.1', require: false + group :development do gem 'pry-byebug', '~> 3.5.1', platform: :mri gem "ruby-debug-ide", "~> 0.7.0" diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 47dd5ac118e..66b635868f8 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -30,13 +30,13 @@ GEM ast (2.4.1) binding_ninja (0.2.3) byebug (9.1.0) - capybara (3.29.0) + capybara (3.35.3) addressable mini_mime (>= 0.1.3) nokogiri (~> 1.8) rack (>= 1.6.0) rack-test (>= 0.6.3) - regexp_parser (~> 1.5) + regexp_parser (>= 1.5, < 3.0) xpath (~> 3.2) capybara-screenshot (1.0.23) capybara (>= 1.0, < 4) @@ -56,6 +56,8 @@ GEM adamantium (~> 0.2.0) equalizer (~> 0.0.9) concurrent-ruby (1.1.9) + deprecation_toolkit (1.5.1) + activesupport (>= 4.2) diff-lcs (1.3) domain_name (0.5.20190701) unf (>= 0.0.5, < 1.0.0) @@ -98,12 +100,12 @@ GEM mime-types (3.3.1) mime-types-data (~> 3.2015) mime-types-data (3.2021.0704) - mini_mime (1.0.2) - mini_portile2 (2.5.0) + mini_mime (1.1.0) + mini_portile2 (2.5.3) minitest (5.14.4) multipart-post (2.1.1) netrc (0.11.0) - nokogiri (1.11.1) + nokogiri (1.11.7) mini_portile2 (~> 2.5.0) racc (~> 1.4) octokit (4.21.0) @@ -141,18 +143,18 @@ GEM netrc (~> 0.8) rexml (3.2.5) rotp (3.1.0) - rspec (3.9.0) - rspec-core (~> 3.9.0) - rspec-expectations (~> 3.9.0) - rspec-mocks (~> 3.9.0) - rspec-core (3.9.3) - rspec-support (~> 3.9.3) - rspec-expectations (3.9.1) + rspec (3.10.0) + rspec-core (~> 3.10.0) + rspec-expectations (~> 3.10.0) + rspec-mocks (~> 3.10.0) + rspec-core (3.10.1) + rspec-support (~> 3.10.0) + rspec-expectations (3.10.1) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) - rspec-mocks (3.9.1) + rspec-support (~> 3.10.0) + rspec-mocks (3.10.2) diff-lcs (>= 1.2.0, < 2.0) - rspec-support (~> 3.9.0) + rspec-support (~> 3.10.0) rspec-parameterized (0.4.2) binding_ninja (>= 0.2.3) parser @@ -161,7 +163,7 @@ GEM unparser rspec-retry (0.6.1) rspec-core (> 3.3) - rspec-support (3.9.4) + rspec-support (3.10.2) rspec_junit_formatter (0.4.1) rspec-core (>= 2, < 4, != 2.12.0) ruby-debug-ide (0.7.2) @@ -211,14 +213,15 @@ DEPENDENCIES activesupport (~> 6.1.3.2) airborne (~> 0.3.4) allure-rspec (~> 2.14.1) - capybara (~> 3.29.0) + capybara (~> 3.35.0) capybara-screenshot (~> 1.0.23) chemlab (~> 0.7) chemlab-library-www-gitlab-com (~> 0.1) + deprecation_toolkit (~> 1.5.1) faker (~> 1.6, >= 1.6.6) gitlab-qa knapsack (~> 1.17) - nokogiri (~> 1.11.1) + nokogiri (~> 1.11.7) octokit (~> 4.21) parallel (~> 1.19) parallel_tests (~> 2.29) @@ -226,7 +229,7 @@ DEPENDENCIES rake (~> 12.3.3) rest-client (~> 2.1.0) rotp (~> 3.1.0) - rspec (~> 3.7) + rspec (~> 3.10) rspec-parameterized (~> 0.4.2) rspec-retry (~> 0.6.1) rspec_junit_formatter (~> 0.4.1) diff --git a/qa/chemlab-library-gitlab.gemspec b/qa/chemlab-library-gitlab.gemspec new file mode 100644 index 00000000000..908aad01850 --- /dev/null +++ b/qa/chemlab-library-gitlab.gemspec @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +$:.unshift(File.expand_path('lib', __dir__)) + +Gem::Specification.new do |spec| + spec.name = 'chemlab-library-gitlab' + spec.version = '0.1.1' + spec.authors = ['GitLab Quality'] + spec.email = ['quality@gitlab.com'] + + spec.required_ruby_version = '>= 2.5' # rubocop:disable Gemspec/RequiredRubyVersion + + spec.summary = 'Chemlab Page Libraries for GitLab' + spec.homepage = 'https://gitlab.com/' + spec.license = 'MIT' + + spec.files = `git ls-files -- lib/*`.split("\n") + + spec.require_paths = ['lib'] + + spec.add_runtime_dependency 'chemlab', '~> 0.7' +end diff --git a/qa/lib/gitlab.rb b/qa/lib/gitlab.rb new file mode 100644 index 00000000000..d0d1d535114 --- /dev/null +++ b/qa/lib/gitlab.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +# Chemlab Page Libraries for GitLab +module Gitlab + module Page + module Main + autoload :Login, 'gitlab/page/main/login' + end + + module Subscriptions + autoload :New, 'gitlab/page/subscriptions/new' + end + + module Group + module Settings + autoload :Billing, 'gitlab/page/group/settings/billing' + end + end + end +end diff --git a/qa/lib/gitlab/page/group/settings/billing.rb b/qa/lib/gitlab/page/group/settings/billing.rb new file mode 100644 index 00000000000..24d327502f8 --- /dev/null +++ b/qa/lib/gitlab/page/group/settings/billing.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Group + module Settings + class Billing < Chemlab::Page + # TODO: Supplant with data-qa-selectors + h4 :billing_plan_header, css: 'div.billing-plan-header h4' + + link :start_your_free_trial + + link :upgrade_to_premium, css: '[data-testid="plan-card-premium"] a.billing-cta-purchase-new' + link :upgrade_to_ultimate, css: '[data-testid="plan-card-ultimate"] a.billing-cta-purchase-new' + end + end + end + end +end diff --git a/qa/lib/gitlab/page/group/settings/billing.stub.rb b/qa/lib/gitlab/page/group/settings/billing.stub.rb new file mode 100644 index 00000000000..64176af794a --- /dev/null +++ b/qa/lib/gitlab/page/group/settings/billing.stub.rb @@ -0,0 +1,107 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Group + module Settings + module Billing + # @note Defined as +h4 :billing_plan_header+ + # @return [String] The text content or value of +billing_plan_header+ + def billing_plan_header + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing.billing_plan_header_element).to exist + # end + # @return [Watir::H4] The raw +H4+ element + def billing_plan_header_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing).to be_billing_plan_header + # end + # @return [Boolean] true if the +billing_plan_header+ element is present on the page + def billing_plan_header? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +link :start_your_free_trial+ + # Clicks +start_your_free_trial+ + def start_your_free_trial + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing.start_your_free_trial_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def start_your_free_trial_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing).to be_start_your_free_trial + # end + # @return [Boolean] true if the +start_your_free_trial+ element is present on the page + def start_your_free_trial? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +link :upgrade_to_premium+ + # Clicks +upgrade_to_premium+ + def upgrade_to_premium + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing.upgrade_to_premium_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def upgrade_to_premium_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing).to be_upgrade_to_premium + # end + # @return [Boolean] true if the +upgrade_to_premium+ element is present on the page + def upgrade_to_premium? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +link :upgrade_to_ultimate+ + # Clicks +upgrade_to_ultimate+ + def upgrade_to_ultimate + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing.upgrade_to_ultimate_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def upgrade_to_ultimate_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Group::Settings::Billing.perform do |billing| + # expect(billing).to be_upgrade_to_ultimate + # end + # @return [Boolean] true if the +upgrade_to_ultimate+ element is present on the page + def upgrade_to_ultimate? + # 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/main/login.rb b/qa/lib/gitlab/page/main/login.rb new file mode 100644 index 00000000000..9f20a040550 --- /dev/null +++ b/qa/lib/gitlab/page/main/login.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Main + class Login < Chemlab::Page + path '/users/sign_in' + + text_field :login_field + text_field :password_field + button :sign_in_button + + def sign_in_as(username:, password:) + self.login_field = username + self.password_field = password + + sign_in_button + end + end + end + end +end diff --git a/qa/lib/gitlab/page/main/login.stub.rb b/qa/lib/gitlab/page/main/login.stub.rb new file mode 100644 index 00000000000..a4cef291616 --- /dev/null +++ b/qa/lib/gitlab/page/main/login.stub.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Main + module Login + # @note Defined as +text_field :login_field+ + # @return [String] The text content or value of +login_field+ + def login_field + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of login_field + # @example + # Gitlab::Page::Main::Login.perform do |login| + # login.login_field = 'value' + # end + # @param value [String] The value to set. + def login_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.login_field_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def login_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_login_field + # end + # @return [Boolean] true if the +login_field+ element is present on the page + def login_field? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :password_field+ + # @return [String] The text content or value of +password_field+ + def password_field + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of password_field + # @example + # Gitlab::Page::Main::Login.perform do |login| + # login.password_field = 'value' + # end + # @param value [String] The value to set. + def password_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_field_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def password_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_field + # end + # @return [Boolean] true if the +password_field+ element is present on the page + def password_field? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +button :sign_in_button+ + # Clicks +sign_in_button+ + def sign_in_button + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Main::Login.perform do |login| + # expect(login.sign_in_button_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def sign_in_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_sign_in_button + # end + # @return [Boolean] true if the +sign_in_button+ element is present on the page + def sign_in_button? + # 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 new file mode 100644 index 00000000000..4c0e5446444 --- /dev/null +++ b/qa/lib/gitlab/page/subscriptions/new.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Subscriptions + class New < Chemlab::Page + path '/subscriptions/new' + + # Subscription Details + select :plan_name + select :group_name + text_field :number_of_users + button :continue_to_billing, text: /Continue to billing/ + + # Billing address + select :country + text_field :street_address_1 + text_field :street_address_2 + text_field :city + select :state + text_field :zip_code + button :continue_to_payment, text: /Continue to payment/ + + # Payment method + # TODO: Revisit when https://gitlab.com/gitlab-org/quality/chemlab/-/issues/6 is closed + iframe :payment_form, id: 'z_hppm_iframe' + + text_field(:name_on_card) { payment_form_element.text_field(id: 'input-creditCardHolderName') } + text_field(:card_number) { payment_form_element.text_field(id: 'input-creditCardNumber') } + select(:expiration_month) { payment_form_element.select(id: 'input-creditCardExpirationMonth') } + select(:expiration_year) { payment_form_element.select(id: 'input-creditCardExpirationYear') } + text_field(:cvv) { payment_form_element.text_field(id: 'input-cardSecurityCode') } + link(:review_your_order) { payment_form_element.link(text: /Review your order/) } + # ENDTODO + + # Confirmation + button :confirm_purchase, text: /Confirm purchase/ + end + end + end +end diff --git a/qa/lib/gitlab/page/subscriptions/new.stub.rb b/qa/lib/gitlab/page/subscriptions/new.stub.rb new file mode 100644 index 00000000000..93680ab95e0 --- /dev/null +++ b/qa/lib/gitlab/page/subscriptions/new.stub.rb @@ -0,0 +1,545 @@ +# frozen_string_literal: true + +module Gitlab + module Page + module Subscriptions + module New + # @note Defined as +select :plan_name+ + # @return [String] The text content or value of +plan_name+ + def plan_name + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.plan_name_element).to exist + # end + # @return [Watir::Select] The raw +Select+ element + def plan_name_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_plan_name + # end + # @return [Boolean] true if the +plan_name+ element is present on the page + def plan_name? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +select :group_name+ + # @return [String] The text content or value of +group_name+ + def group_name + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.group_name_element).to exist + # end + # @return [Watir::Select] The raw +Select+ element + def group_name_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_group_name + # end + # @return [Boolean] true if the +group_name+ element is present on the page + def group_name? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :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 + + # Set the value of number_of_users + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.number_of_users = 'value' + # end + # @param value [String] The value to set. + def number_of_users=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.number_of_users_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def number_of_users_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).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 + + # @note Defined as +button :continue_to_billing+ + # Clicks +continue_to_billing+ + def continue_to_billing + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.continue_to_billing_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def continue_to_billing_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_continue_to_billing + # end + # @return [Boolean] true if the +continue_to_billing+ element is present on the page + def continue_to_billing? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +select :country+ + # @return [String] The text content or value of +country+ + def country + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.country_element).to exist + # end + # @return [Watir::Select] The raw +Select+ element + def country_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_country + # end + # @return [Boolean] true if the +country+ element is present on the page + def country? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :street_address_1+ + # @return [String] The text content or value of +street_address_1+ + def street_address_1 + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of street_address_1 + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.street_address_1 = 'value' + # end + # @param value [String] The value to set. + def street_address_1=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.street_address_1_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def street_address_1_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_street_address_1 + # end + # @return [Boolean] true if the +street_address_1+ element is present on the page + def street_address_1? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :street_address_2+ + # @return [String] The text content or value of +street_address_2+ + def street_address_2 + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of street_address_2 + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.street_address_2 = 'value' + # end + # @param value [String] The value to set. + def street_address_2=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.street_address_2_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def street_address_2_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_street_address_2 + # end + # @return [Boolean] true if the +street_address_2+ element is present on the page + def street_address_2? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :city+ + # @return [String] The text content or value of +city+ + def city + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of city + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.city = 'value' + # end + # @param value [String] The value to set. + def city=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.city_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def city_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_city + # end + # @return [Boolean] true if the +city+ element is present on the page + def city? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +select :state+ + # @return [String] The text content or value of +state+ + def state + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.state_element).to exist + # end + # @return [Watir::Select] The raw +Select+ element + def state_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_state + # end + # @return [Boolean] true if the +state+ element is present on the page + def state? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :zip_code+ + # @return [String] The text content or value of +zip_code+ + def zip_code + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of zip_code + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.zip_code = 'value' + # end + # @param value [String] The value to set. + def zip_code=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.zip_code_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def zip_code_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_zip_code + # end + # @return [Boolean] true if the +zip_code+ element is present on the page + def zip_code? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +button :continue_to_payment+ + # Clicks +continue_to_payment+ + def continue_to_payment + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.continue_to_payment_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def continue_to_payment_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_continue_to_payment + # end + # @return [Boolean] true if the +continue_to_payment+ element is present on the page + def continue_to_payment? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +iframe :payment_form+ + # @return [String] The text content or value of +payment_form+ + def payment_form + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.payment_form_element).to exist + # end + # @return [Watir::Iframe] The raw +Iframe+ element + def payment_form_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_payment_form + # end + # @return [Boolean] true if the +payment_form+ element is present on the page + def payment_form? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :name_on_card+ + # @return [String] The text content or value of +name_on_card+ + def name_on_card + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of name_on_card + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.name_on_card = 'value' + # end + # @param value [String] The value to set. + def name_on_card=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.name_on_card_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def name_on_card_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_name_on_card + # end + # @return [Boolean] true if the +name_on_card+ element is present on the page + def name_on_card? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :card_number+ + # @return [String] The text content or value of +card_number+ + def card_number + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of card_number + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.card_number = 'value' + # end + # @param value [String] The value to set. + def card_number=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.card_number_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def card_number_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_card_number + # end + # @return [Boolean] true if the +card_number+ element is present on the page + def card_number? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +select :expiration_month+ + # @return [String] The text content or value of +expiration_month+ + def expiration_month + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.expiration_month_element).to exist + # end + # @return [Watir::Select] The raw +Select+ element + def expiration_month_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_expiration_month + # end + # @return [Boolean] true if the +expiration_month+ element is present on the page + def expiration_month? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +select :expiration_year+ + # @return [String] The text content or value of +expiration_year+ + def expiration_year + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.expiration_year_element).to exist + # end + # @return [Watir::Select] The raw +Select+ element + def expiration_year_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_expiration_year + # end + # @return [Boolean] true if the +expiration_year+ element is present on the page + def expiration_year? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :cvv+ + # @return [String] The text content or value of +cvv+ + def cvv + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of cvv + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # new.cvv = 'value' + # end + # @param value [String] The value to set. + def cvv=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.cvv_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def cvv_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_cvv + # end + # @return [Boolean] true if the +cvv+ element is present on the page + def cvv? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +link :review_your_order+ + # Clicks +review_your_order+ + def review_your_order + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.review_your_order_element).to exist + # end + # @return [Watir::Link] The raw +Link+ element + def review_your_order_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_review_your_order + # end + # @return [Boolean] true if the +review_your_order+ element is present on the page + def review_your_order? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +button :confirm_purchase+ + # Clicks +confirm_purchase+ + def confirm_purchase + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new.confirm_purchase_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def confirm_purchase_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Subscriptions::New.perform do |new| + # expect(new).to be_confirm_purchase + # end + # @return [Boolean] true if the +confirm_purchase+ element is present on the page + def confirm_purchase? + # This is a stub, used for indexing. The method is dynamically generated. + end + end + end + end +end @@ -8,6 +8,8 @@ require_relative '../lib/gitlab' require_relative '../lib/gitlab/utils' require_relative '../config/initializers/0_inject_enterprise_edition_module' +require_relative 'lib/gitlab' + require 'chemlab' module QA @@ -71,6 +73,7 @@ module QA autoload :GroupBase, 'qa/resource/group_base' autoload :Sandbox, 'qa/resource/sandbox' autoload :Group, 'qa/resource/group' + autoload :BulkImportGroup, 'qa/resource/bulk_import_group' autoload :Issue, 'qa/resource/issue' autoload :ProjectIssueNote, 'qa/resource/project_issue_note' autoload :Project, 'qa/resource/project' @@ -236,6 +239,7 @@ module QA autoload :Menu, 'qa/page/group/menu' autoload :Members, 'qa/page/group/members' autoload :BulkImport, 'qa/page/group/bulk_import' + autoload :DependencyProxy, 'qa/page/group/dependency_proxy' module Milestone autoload :Index, 'qa/page/group/milestone/index' @@ -385,7 +389,6 @@ module QA module Deployments module Environments autoload :Index, 'qa/page/project/deployments/environments/index' - autoload :Show, 'qa/page/project/deployments/environments/show' end end @@ -424,6 +427,10 @@ module QA autoload :Show, 'qa/page/project/snippet/show' autoload :Index, 'qa/page/project/snippet/index' end + + module Secure + autoload :ConfigurationForm, 'qa/page/project/secure/configuration_form' + end end module Profile @@ -538,6 +545,7 @@ module QA autoload :AccessTokens, 'qa/page/component/access_tokens' autoload :CommitModal, 'qa/page/component/commit_modal' autoload :VisibilitySetting, 'qa/page/component/visibility_setting' + autoload :ContentEditor, 'qa/page/component/content_editor' module Import autoload :Gitlab, 'qa/page/component/import/gitlab' @@ -627,7 +635,9 @@ module QA module Helpers autoload :ContextSelector, 'qa/specs/helpers/context_selector' + autoload :ContextFormatter, 'qa/specs/helpers/context_formatter' autoload :Quarantine, 'qa/specs/helpers/quarantine' + autoload :QuarantineFormatter, 'qa/specs/helpers/quarantine_formatter' autoload :RSpec, 'qa/specs/helpers/rspec' end end @@ -675,6 +685,7 @@ module QA autoload :WaitForRequests, 'qa/support/wait_for_requests' autoload :OTP, 'qa/support/otp' autoload :SSH, 'qa/support/ssh' + autoload :AllureMetadataFormatter, 'qa/support/allure_metadata_formatter.rb' end end diff --git a/qa/qa/fixtures/export.tar.gz b/qa/qa/fixtures/export.tar.gz Binary files differindex 08e4f0c9c43..8d27b6816ea 100644 --- a/qa/qa/fixtures/export.tar.gz +++ b/qa/qa/fixtures/export.tar.gz diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 6b54d8ab1ac..9debdc1d4dd 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -21,7 +21,7 @@ module QA end def to_s - <<~MSG.strip % { page: @page_class } + format(<<~MSG.strip, page: @page_class) %{page} has no required elements. See https://docs.gitlab.com/ee/development/testing_guide/end_to_end/dynamic_element_validation.html#required-elements MSG @@ -108,7 +108,7 @@ module QA wait_for_requests(skip_finished_loading_check: skip_finished_loading_check) element_selector = element_selector_css(name, reject_capybara_query_keywords(kwargs)) - find(element_selector, only_capybara_query_keywords(kwargs)) + find(element_selector, **only_capybara_query_keywords(kwargs)) end def only_capybara_query_keywords(kwargs) @@ -232,11 +232,11 @@ module QA visible = kwargs.delete(:visible) visible = visible.nil? && true - try_find_element = ->(wait) do + try_find_element = lambda do |wait| if disabled.nil? has_css?(element_selector_css(name, kwargs), text: text, wait: wait, class: klass, visible: visible) else - find_element(name, original_kwargs).disabled? == disabled + find_element(name, **original_kwargs).disabled? == disabled end end @@ -322,13 +322,13 @@ module QA # It would be ideal if we could detect when the animation is complete # but in some cases there's nothing we can easily access via capybara # so instead we wait for the element, and then we wait a little longer - raise ElementNotFound, %Q(Couldn't find element named "#{name}") unless has_element?(name) + raise ElementNotFound, %(Couldn't find element named "#{name}") unless has_element?(name) sleep 1 end def within_element(name, **kwargs) - wait_for_requests + wait_for_requests(skip_finished_loading_check: kwargs.delete(:skip_finished_loading_check)) text = kwargs.delete(:text) page.within(element_selector_css(name, kwargs), text: text) do @@ -386,9 +386,7 @@ module QA end def self.errors - if views.empty? - return ["Page class does not have views / elements defined!"] - end + return ["Page class does not have views / elements defined!"] if views.empty? views.flat_map(&:errors) end diff --git a/qa/qa/page/component/content_editor.rb b/qa/qa/page/component/content_editor.rb new file mode 100644 index 00000000000..b3a42634fe7 --- /dev/null +++ b/qa/qa/page/component/content_editor.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module ContentEditor + extend QA::Page::PageConcern + + def self.included(base) + super + + base.view 'app/assets/javascripts/content_editor/components/content_editor.vue' do + element :content_editor_container + end + + base.view 'app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue' do + element :text_style_dropdown + element :text_style_menu_item + end + + base.view 'app/assets/javascripts/content_editor/components/toolbar_image_button.vue' do + element :file_upload_field + end + end + + def add_heading(heading, text) + within_element(:content_editor_container) do + text_area.set(text) + # wait for text style option to become active after typing + has_active_element?(:text_style_dropdown, wait: 1) + click_element(:text_style_dropdown) + within_element(:text_style_dropdown) do + click_element(:text_style_menu_item, text_style: heading) + end + end + end + + def upload_image(image_path) + within_element(:content_editor_container) do + # add image on a new line + text_area.send_keys(:return) + find_element(:file_upload_field, visible: false).send_keys(image_path) + end + end + + private + + def text_area + find('[contenteditable="true"]', visible: false) + end + end + end + end +end diff --git a/qa/qa/page/component/import/gitlab.rb b/qa/qa/page/component/import/gitlab.rb index 2fd2a45b399..5831a713ae5 100644 --- a/qa/qa/page/component/import/gitlab.rb +++ b/qa/qa/page/component/import/gitlab.rb @@ -5,6 +5,8 @@ module QA module Component module Import module Gitlab + extend QA::Page::PageConcern + def self.included(base) super @@ -30,7 +32,7 @@ module QA click_element(:import_project_button) wait_until(reload: false) do - has_notice?("The project was successfully imported.") + has_notice?("The project was successfully imported.") || has_element?(:project_name_content) end end end diff --git a/qa/qa/page/component/invite_members_modal.rb b/qa/qa/page/component/invite_members_modal.rb index 7cec4588af5..fecd61fb410 100644 --- a/qa/qa/page/component/invite_members_modal.rb +++ b/qa/qa/page/component/invite_members_modal.rb @@ -40,18 +40,20 @@ module QA click_element :invite_a_group_button end - def add_member(username, access_level = Resource::Members::AccessLevel::DEVELOPER) + def add_member(username, access_level = 'Developer') open_invite_members_modal within_element(:invite_members_modal_content) do - fill_element :access_level_dropdown, with: access_level - fill_element :members_token_select_input, username - Support::WaitForRequests.wait_for_requests - click_button username + # Guest option is selected by default, skipping these steps if desired option is 'Guest' + unless access_level == 'Guest' + click_element :access_level_dropdown + click_button access_level + end + click_element :invite_button end diff --git a/qa/qa/page/component/wiki.rb b/qa/qa/page/component/wiki.rb index 92eb25af247..c3db1d6c885 100644 --- a/qa/qa/page/component/wiki.rb +++ b/qa/qa/page/component/wiki.rb @@ -68,6 +68,18 @@ module QA def has_no_page? has_element?(:create_first_page_link) end + + def has_heading?(heading_type, text) + within_element(:wiki_page_content) do + has_css?(heading_type, text: text) + end + end + + def has_image?(image_file_name) + within_element(:wiki_page_content) do + has_css?("img[src$='#{image_file_name}']") + end + end end end end diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb index bb22b7da003..6b7452b0e0f 100644 --- a/qa/qa/page/component/wiki_page_form.rb +++ b/qa/qa/page/component/wiki_page_form.rb @@ -14,6 +14,7 @@ module QA element :wiki_content_textarea element :wiki_message_textbox element :wiki_submit_button + element :try_new_editor_container end base.view 'app/assets/javascripts/pages/shared/wikis/components/delete_wiki_modal.vue' do @@ -41,6 +42,12 @@ module QA click_element(:delete_button, Page::Modal::DeleteWiki) Page::Modal::DeleteWiki.perform(&:confirm_deletion) end + + def use_new_editor + within_element(:try_new_editor_container) do + click_button('Use the new editor') + end + end end end end diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 8be11550233..c0108d85365 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -13,6 +13,10 @@ module QA element :user_role_content end + view 'app/views/dashboard/_projects_head.html.haml' do + element :new_project_button + end + def has_project_with_access_role?(project_name, access_role) within_element(:project_content, text: project_name) do has_element?(:user_role_content, text: access_role) @@ -25,6 +29,10 @@ module QA find_link(text: name).click end + def click_new_project_button + click_element(:new_project_button, Page::Project::New) + end + def self.path '/' end diff --git a/qa/qa/page/group/bulk_import.rb b/qa/qa/page/group/bulk_import.rb index a0511c9a16c..9ba80abf21c 100644 --- a/qa/qa/page/group/bulk_import.rb +++ b/qa/qa/page/group/bulk_import.rb @@ -6,13 +6,13 @@ module QA class BulkImport < Page::Base view "app/assets/javascripts/import_entities/import_groups/components/import_table.vue" do element :import_table + element :import_item + element :import_group_button + element :import_status_indicator end - view "app/assets/javascripts/import_entities/import_groups/components/import_table_row.vue" do - element :import_item + view "app/assets/javascripts/import_entities/import_groups/components/import_target_cell.vue" do element :target_group_dropdown_item - element :import_status_indicator - element :import_group_button end view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do diff --git a/qa/qa/page/group/dependency_proxy.rb b/qa/qa/page/group/dependency_proxy.rb new file mode 100644 index 00000000000..f637c79cffc --- /dev/null +++ b/qa/qa/page/group/dependency_proxy.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +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 + 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 + end + end + end +end diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb index b526a4488b2..ccc901932f4 100644 --- a/qa/qa/page/group/members.rb +++ b/qa/qa/page/group/members.rb @@ -7,7 +7,7 @@ module QA include Page::Component::InviteMembersModal include Page::Component::UsersSelect - view 'app/assets/javascripts/vue_shared/components/remove_member_modal.vue' do + view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do element :remove_member_modal_content end diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb index 338a135614d..c997598e25a 100644 --- a/qa/qa/page/group/menu.rb +++ b/qa/qa/page/group/menu.rb @@ -6,51 +6,32 @@ module QA class Menu < Page::Base include SubMenus::Common - view 'app/views/layouts/nav/sidebar/_group_menus.html.haml' do - element :general_settings_link - element :group_issues_item - element :group_members_item - element :group_milestones_link - element :group_settings - element :group_information_link - element :group_information_submenu - end - - view 'app/views/groups/sidebar/_packages_settings.html.haml' do - element :group_package_settings_link - end - - view 'app/views/layouts/nav/sidebar/_analytics_links.html.haml' do - element :analytics_link - element :analytics_sidebar_submenu - end - def click_group_members_item - hover_element(:group_information_link) do - within_submenu(:group_information_submenu) do - click_element(:group_members_item) + hover_group_information do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Members') end end end - def click_settings - within_sidebar do - click_element(:group_settings) + def click_subgroup_members_item + hover_subgroup_information do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Members') + end end end - def click_contribution_analytics_item - hover_element(:analytics_link) do - within_submenu(:analytics_sidebar_submenu) do - click_element(:contribution_analytics_link) - end + def click_settings + within_sidebar do + click_element(:sidebar_menu_link, menu_item: 'Settings') end end def click_group_general_settings_item - hover_element(:group_settings) do - within_submenu(:group_sidebar_submenu) do - click_element(:general_settings_link) + hover_group_settings do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'General') end end end @@ -58,16 +39,31 @@ module QA def go_to_milestones hover_issues do within_submenu do - click_element(:group_milestones_link) + click_element(:sidebar_menu_item_link, menu_item: 'Milestones') end end end def go_to_package_settings - scroll_to_element(:group_settings) - hover_element(:group_settings) do - within_submenu(:group_sidebar_submenu) do - click_element(:group_package_settings_link) + hover_group_settings do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Packages & Registries') + end + end + end + + def go_to_group_packages + hover_group_packages do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Package Registry') + end + end + end + + def go_to_dependency_proxy + hover_group_packages do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Dependency Proxy') end end end @@ -76,8 +72,42 @@ module QA def hover_issues within_sidebar do - scroll_to_element(:group_issues_item) - find_element(:group_issues_item).hover + scroll_to_element(:sidebar_menu_link, menu_item: 'Issues') + find_element(:sidebar_menu_link, menu_item: 'Issues').hover + + yield + end + end + + def hover_group_information + within_sidebar do + find_element(:sidebar_menu_link, menu_item: 'Group information').hover + + yield + end + end + + def hover_subgroup_information + within_sidebar do + find_element(:sidebar_menu_link, menu_item: 'Subgroup information').hover + + yield + end + end + + def hover_group_packages + within_sidebar do + scroll_to_element(:sidebar_menu_link, menu_item: 'Packages & Registries') + find_element(:sidebar_menu_link, menu_item: 'Packages & Registries').hover + + yield + end + end + + def hover_group_settings + within_sidebar do + scroll_to_element(:sidebar_menu_link, menu_item: 'Settings') + find_element(:sidebar_menu_link, menu_item: 'Settings').hover yield end diff --git a/qa/qa/page/group/settings/billing.rb b/qa/qa/page/group/settings/billing.rb deleted file mode 100644 index a83af47fc35..00000000000 --- a/qa/qa/page/group/settings/billing.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Group - module Settings - class Billing < Chemlab::Page - link :start_your_free_trial - end - end - end - end -end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 5f52d48e9f6..afe88fc0cdc 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -202,7 +202,9 @@ module QA def has_pipeline_status?(text) # Pipelines can be slow, so we wait a bit longer than the usual 10 seconds - has_element?(:merge_request_pipeline_info_content, text: text, wait: 60) + wait_until(sleep_interval: 5, reload: false) do + has_element?(:merge_request_pipeline_info_content, text: text, wait: 15 ) + end end def has_title?(title) @@ -236,7 +238,10 @@ module QA end def merged? - # Revisit after merge page re-architect is done https://gitlab.com/gitlab-org/gitlab/-/issues/300042 + # Reloads the page at this point to avoid the problem of the merge status failing to update + # That's the transient UX issue this test is checking for, so if the MR is merged but the UI still shows the + # status as unmerged, the test will fail. + # Revisit after merge page re-architect is done https://gitlab.com/groups/gitlab-org/-/epics/5598 # To remove page refresh logic if possible retry_until(max_attempts: 3, reload: true) do has_element?(:merged_status_content, text: 'The changes were merged into', wait: 20) diff --git a/qa/qa/page/project/deployments/environments/show.rb b/qa/qa/page/project/deployments/environments/show.rb deleted file mode 100644 index 48e4850d3be..00000000000 --- a/qa/qa/page/project/deployments/environments/show.rb +++ /dev/null @@ -1,23 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Project - module Deployments - module Environments - class Show < Page::Base - view 'app/views/projects/environments/_external_url.html.haml' do - element :view_deployment - end - - def view_deployment(&block) - new_window = window_opened_by { click_element(:view_deployment) } - - within_window(new_window, &block) if block - end - end - end - end - end - end -end diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb index 74bc4cec467..bb35c5eb17c 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -18,12 +18,17 @@ module QA element :import_button element :project_path_content element :go_to_project_button + element :import_status_indicator end view "app/assets/javascripts/import_entities/components/group_dropdown.vue" do element :target_namespace_selector_dropdown end + # Add personal access token + # + # @param [String] personal_access_token + # @return [void] def add_personal_access_token(personal_access_token) # If for some reasons this process is retried, user cannot re-enter github token in the same group # In this case skip this step and proceed to import project row @@ -34,71 +39,50 @@ module QA finished_loading? end - def import!(full_path, name) - return if already_imported(full_path) - - choose_test_namespace(full_path) - set_path(full_path, name) - import_project(full_path) - - wait_for_success - end - - # TODO: refactor to use 'go to project' button instead of generic main menu - def go_to_project(name) - Page::Main::Menu.perform(&:go_to_projects) - Page::Dashboard::Projects.perform do |dashboard| - dashboard.go_to_project(name) - end - end - - private - - def within_repo_path(full_path, &block) - project_import_row = find_element(:project_import_row, text: full_path) - - within(project_import_row, &block) - end - - def choose_test_namespace(full_path) - within_repo_path(full_path) do - within_element(:target_namespace_selector_dropdown) { click_button(class: 'dropdown-toggle') } - click_element(:target_group_dropdown_item, group_name: Runtime::Namespace.path) - end - end - - def set_path(full_path, name) - within_repo_path(full_path) do - fill_element(:project_path_field, name) - end - end - - def import_project(full_path) - within_repo_path(full_path) do + # Import project + # + # @param [String] source_project_name + # @param [String] target_group_path + # @return [void] + def import!(gh_project_name, target_group_path, project_name) + within_element(:project_import_row, source_project: gh_project_name) do + click_element(:target_namespace_selector_dropdown) + click_element(:target_group_dropdown_item, group_name: target_group_path) + fill_element(:project_path_field, project_name) click_element(:import_button) end end - def wait_for_success - # TODO: set reload:false and remove skip_finished_loading_check_on_refresh when - # https://gitlab.com/gitlab-org/gitlab/-/issues/292861 is fixed - wait_until( - max_duration: 90, - sleep_interval: 5.0, - reload: true, - skip_finished_loading_check_on_refresh: true - ) do - # TODO: Refactor to explicitly wait for specific project import successful status - # This check can create false positive if main importing message appears with delay and check exits early - page.has_no_content?('Importing 1 repository', wait: 3) + # Check Go to project button present + # + # @param [String] gh_project_name + # @return [Boolean] + def has_go_to_project_button?(gh_project_name) + within_element(:project_import_row, source_project: gh_project_name) do + has_element?(:go_to_project_button) end end - def already_imported(full_path) - within_repo_path(full_path) do - has_element?(:project_path_content) && has_element?(:go_to_project_button) + # Check if import page has a successfully imported project + # + # @param [String] source_project_name + # @param [Integer] wait + # @return [Boolean] + def has_imported_project?(gh_project_name, wait: QA::Support::WaitForRequests::DEFAULT_MAX_WAIT_TIME) + within_element(:project_import_row, source_project: gh_project_name, skip_finished_loading_check: true) do + # TODO: remove retrier with reload:true once https://gitlab.com/gitlab-org/gitlab/-/issues/292861 is fixed + wait_until( + max_duration: wait, + sleep_interval: 5, + reload: true, + skip_finished_loading_check_on_refresh: true + ) do + has_element?(:import_status_indicator, text: "Complete") + end end end + + alias_method :wait_for_success, :has_imported_project? end end end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 170cc14b27f..06e476f009a 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -83,8 +83,8 @@ module QA click_button 'Repo by URL' end - def enable_initialize_with_readme - check_element(:initialize_with_readme_checkbox) + def disable_initialize_with_readme + uncheck_element(:initialize_with_readme_checkbox) end end end diff --git a/qa/qa/page/project/registry/show.rb b/qa/qa/page/project/registry/show.rb index dffdb9eebba..03c547fc8b5 100644 --- a/qa/qa/page/project/registry/show.rb +++ b/qa/qa/page/project/registry/show.rb @@ -31,7 +31,7 @@ module QA def click_delete click_element(:tag_delete_button) - find_button('Confirm').click + find_button('Delete').click end end end diff --git a/qa/qa/page/project/secure/configuration_form.rb b/qa/qa/page/project/secure/configuration_form.rb new file mode 100644 index 00000000000..73d1601b61e --- /dev/null +++ b/qa/qa/page/project/secure/configuration_form.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Secure + class ConfigurationForm < QA::Page::Base + include QA::Page::Component::Select2 + include QA::Page::Settings::Common + + view 'app/assets/javascripts/security_configuration/components/feature_card.vue' do + element :sast_status, "`${feature.type}_status`" # rubocop:disable QA/ElementWithPattern + element :sast_enable_button, "`${feature.type}_enable_button`" # rubocop:disable QA/ElementWithPattern + end + + def click_sast_enable_button + click_element(:sast_enable_button) + end + + def has_sast_status?(status_text) + within_element(:sast_status) do + has_text?(status_text) + end + end + end + end + end + end +end + +QA::Page::Project::Secure::ConfigurationForm.prepend_mod_with('Page::Project::Secure::ConfigurationForm', namespace: QA) diff --git a/qa/qa/page/project/wiki/edit.rb b/qa/qa/page/project/wiki/edit.rb index 70aa10cc43e..e782bbbb432 100644 --- a/qa/qa/page/project/wiki/edit.rb +++ b/qa/qa/page/project/wiki/edit.rb @@ -7,6 +7,7 @@ module QA class Edit < Base include Page::Component::WikiPageForm include Page::Component::WikiSidebar + include Page::Component::ContentEditor end end end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index ca0087cf709..2848e3ba7d2 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -75,19 +75,18 @@ module QA end def log_fabrication(method, resource, parents, args) - return yield unless Runtime::Env.debug? - start = Time.now - prefix = "==#{'=' * parents.size}>" - msg = [prefix] - msg << "Built a #{name}" - msg << "as a dependency of #{parents.last}" if parents.any? - msg << "via #{method}" yield.tap do - msg << "in #{Time.now - start} seconds" - puts msg.join(' ') - puts if parents.empty? + Runtime::Logger.debug do + msg = ["==#{'=' * parents.size}>"] + msg << "Built a #{name}" + msg << "as a dependency of #{parents.last}" if parents.any? + msg << "via #{method}" + msg << "in #{Time.now - start} seconds" + + msg.join(' ') + end end end @@ -189,7 +188,7 @@ module QA end def log_having_both_api_result_and_block(name, api_value) - QA::Runtime::Logger.info(<<~MSG.strip) + QA::Runtime::Logger.debug(<<~MSG.strip) <#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored. MSG end diff --git a/qa/qa/resource/bulk_import_group.rb b/qa/qa/resource/bulk_import_group.rb new file mode 100644 index 00000000000..5380bb16f10 --- /dev/null +++ b/qa/qa/resource/bulk_import_group.rb @@ -0,0 +1,85 @@ +# frozen_string_literal: true + +module QA + module Resource + class BulkImportGroup < Group + attributes :source_group_path, + :import_id + + attribute :destination_group_path do + source_group_path + end + + attribute :access_token do + api_client.personal_access_token + end + + alias_method :path, :source_group_path + + delegate :gitlab_address, to: 'QA::Runtime::Scenario' + + def fabricate_via_browser_ui! + Page::Main::Menu.perform(&:go_to_create_group) + + Page::Group::New.perform do |group| + group.switch_to_import_tab + group.connect_gitlab_instance(gitlab_address, api_client.personal_access_token) + end + + Page::Group::BulkImport.perform do |import_page| + import_page.import_group(path, sandbox.path) + end + + reload! + visit! + end + + def fabricate_via_api! + resource_web_url(api_post) + end + + def api_post_path + '/bulk_imports' + end + + def api_post_body + { + configuration: { + url: gitlab_address, + access_token: access_token + }, + entities: [ + { + source_type: 'group_entity', + source_full_path: source_group_path, + destination_name: destination_group_path, + destination_namespace: sandbox.path + } + ] + } + end + + def import_status + response = get(Runtime::API::Request.new(api_client, "/bulk_imports/#{import_id}").url) + + unless response.code == HTTP_STATUS_OK + raise ResourceQueryError, "Could not get import status. Request returned (#{response.code}): `#{response}`." + end + + parse_body(response)[:status] + end + + private + + def transform_api_resource(api_resource) + return api_resource if api_resource[:web_url] + + # override transformation only for /bulk_imports endpoint which doesn't have web_url in response and + # ignore others so import_id is not overwritten incorrectly + api_resource[:web_url] = "#{gitlab_address}/#{full_path}" + api_resource[:import_id] = api_resource[:id] + api_resource + end + end + end +end diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb index 652c6cf7d1e..b937b704613 100644 --- a/qa/qa/resource/group_base.rb +++ b/qa/qa/resource/group_base.rb @@ -101,3 +101,5 @@ module QA end end end + +QA::Resource::GroupBase.prepend_mod_with('Resource::GroupBase', namespace: QA) diff --git a/qa/qa/resource/issue.rb b/qa/qa/resource/issue.rb index ffffa0eecda..c45ab7593b6 100644 --- a/qa/qa/resource/issue.rb +++ b/qa/qa/resource/issue.rb @@ -14,11 +14,11 @@ module QA end end - attribute :id - attribute :iid - attribute :assignee_ids - attribute :labels - attribute :title + attributes :id, + :iid, + :assignee_ids, + :labels, + :title def initialize @assignee_ids = [] @@ -41,13 +41,21 @@ module QA end def api_get_path - "/projects/#{project.id}/issues/#{id}" + "/projects/#{project.id}/issues/#{iid}" end def api_post_path "/projects/#{project.id}/issues" end + def api_put_path + "/projects/#{project.id}/issues/#{iid}" + end + + def api_comments_path + "#{api_get_path}/notes" + end + def api_post_body { assignee_ids: assignee_ids, @@ -59,20 +67,31 @@ module QA end end - def api_put_path - "/projects/#{project.id}/issues/#{iid}" - end - def set_issue_assignees(assignee_ids:) put_body = { assignee_ids: assignee_ids } response = put Runtime::API::Request.new(api_client, api_put_path).url, put_body unless response.code == HTTP_STATUS_OK - raise ResourceUpdateFailedError, "Could not update issue assignees to #{assignee_ids}. Request returned (#{response.code}): `#{response}`." + raise( + ResourceUpdateFailedError, + "Could not update issue assignees to #{assignee_ids}. Request returned (#{response.code}): `#{response}`." + ) end QA::Runtime::Logger.debug("Successfully updated issue assignees to #{assignee_ids}") end + + # Get issue comments + # + # @return [Array] + def comments(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_comments_path)) unless auto_paginate + + auto_paginated_response( + Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url, + attempts: attempts + ) + end end end end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 8d9de0ea718..8c313f5d518 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -152,7 +152,8 @@ module QA @project = Resource::ImportProject.fabricate_via_browser_ui! # Setting the name here, since otherwise some tests will look for an existing file in # the proejct without ever knowing what is in it. - @file_name = "github_controller_spec.rb" + @file_name = "added_file-00000000.txt" + @source_branch = "large_merge_request" visit("#{project.web_url}/-/merge_requests/1") current_url end @@ -160,9 +161,13 @@ module QA # Get MR comments # # @return [Array] - def comments - response = get(Runtime::API::Request.new(api_client, api_comments_path).url) - parse_body(response) + def comments(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_comments_path)) unless auto_paginate + + auto_paginated_response( + Runtime::API::Request.new(api_client, api_comments_path, per_page: '100').url, + attempts: attempts + ) end private diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index d111b070863..53b8a9b0246 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -13,13 +13,14 @@ module QA :initialize_with_readme, :auto_devops_enabled, :github_personal_access_token, - :github_repository_path + :github_repository_path, + :gitlab_repository_path attributes :id, :name, :add_name_uuid, :description, - :standalone, + :personal_namespace, :runners_token, :visibility, :template_name, @@ -32,7 +33,7 @@ module QA end attribute :path_with_namespace do - "#{sandbox_path}#{group.path}/#{name}" if group + "#{group.full_path}/#{name}" end alias_method :full_path, :path_with_namespace @@ -51,7 +52,7 @@ module QA def initialize @add_name_uuid = true - @standalone = false + @personal_namespace = false @description = 'My awesome project' @initialize_with_readme = false @auto_devops_enabled = false @@ -69,7 +70,9 @@ module QA def fabricate! return if @import - unless @standalone + if @personal_namespace + Page::Dashboard::Projects.perform(&:click_new_project_button) + else group.visit! Page::Group::Show.perform(&:go_to_new_project) end @@ -84,13 +87,15 @@ module QA Page::Project::New.perform(&:click_blank_project_link) Page::Project::New.perform do |new_page| - new_page.choose_test_namespace + new_page.choose_test_namespace unless @personal_namespace new_page.choose_name(@name) new_page.add_description(@description) new_page.set_visibility(@visibility) - new_page.enable_initialize_with_readme if @initialize_with_readme + new_page.disable_initialize_with_readme unless @initialize_with_readme new_page.create_new_project end + + @id = Page::Project::Show.perform(&:project_id) end def fabricate_via_api! @@ -218,7 +223,7 @@ module QA auto_devops_enabled: @auto_devops_enabled } - unless @standalone + unless @personal_namespace post_body[:namespace_id] = group.id post_body[:path] = name end @@ -263,19 +268,24 @@ module QA result = parse_body(response) - Runtime::Logger.error("Import failed: #{result[:import_error]}") if result[:import_status] == "failed" + if result[:import_status] == "failed" + Runtime::Logger.error("Import failed: #{result[:import_error]}") + Runtime::Logger.error("Failed relations: #{result[:failed_relations]}") + end result[:import_status] end - def commits - response = get(request_url(api_commits_path)) - parse_body(response) + def commits(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_commits_path)) unless auto_paginate + + auto_paginated_response(request_url(api_commits_path, per_page: '100'), attempts: attempts) end - def merge_requests - response = get(request_url(api_merge_requests_path)) - parse_body(response) + def merge_requests(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_merge_requests_path)) unless auto_paginate + + auto_paginated_response(request_url(api_merge_requests_path, per_page: '100'), attempts: attempts) end def merge_request_with_title(title) @@ -299,9 +309,10 @@ module QA parse_body(response) end - def repository_branches - response = get(request_url(api_repository_branches_path)) - parse_body(response) + def repository_branches(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_repository_branches_path)) unless auto_paginate + + auto_paginated_response(request_url(api_repository_branches_path, per_page: '100'), attempts: attempts) end def repository_tags @@ -324,19 +335,22 @@ module QA parse_body(response) end - def issues - response = get(request_url(api_issues_path)) - parse_body(response) + def issues(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_issues_path)) unless auto_paginate + + auto_paginated_response(request_url(api_issues_path, per_page: '100'), attempts: attempts) end - def labels - response = get(request_url(api_labels_path)) - parse_body(response) + def labels(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_labels_path)) unless auto_paginate + + auto_paginated_response(request_url(api_labels_path, per_page: '100'), attempts: attempts) end - def milestones - response = get(request_url(api_milestones_path)) - parse_body(response) + def milestones(auto_paginate: false, attempts: 0) + return parse_body(api_get_from(api_milestones_path)) unless auto_paginate + + auto_paginated_response(request_url(api_milestones_path, per_page: '100'), attempts: attempts) end def wikis diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb index 214e8f517bb..8aa19555d50 100644 --- a/qa/qa/resource/project_imported_from_github.rb +++ b/qa/qa/resource/project_imported_from_github.rb @@ -18,9 +18,12 @@ module QA Page::Project::Import::Github.perform do |import_page| import_page.add_personal_access_token(github_personal_access_token) - import_page.import!(github_repository_path, name) - import_page.go_to_project(name) + import_page.import!(github_repository_path, group.full_path, name) + import_page.wait_for_success(github_repository_path) end + + reload! + visit! end def go_to_import_page diff --git a/qa/qa/runtime/allure_report.rb b/qa/qa/runtime/allure_report.rb index bcfdb09e09f..bf49141566a 100644 --- a/qa/qa/runtime/allure_report.rb +++ b/qa/qa/runtime/allure_report.rb @@ -67,25 +67,8 @@ module QA # @return [void] def configure_rspec RSpec.configure do |config| - config.formatter = AllureRspecFormatter - - config.after do |example| - next if example.attempts && example.attempts > 0 - - testcase = example.metadata[:testcase] - example.tms('Testcase', testcase) if testcase - - quarantine_issue = example.metadata.dig(:quarantine, :issue) - example.issue('Quarantine issue', quarantine_issue) if quarantine_issue - - spec_file = example.file_path.split('/').last - example.issue( - 'Failure issues', - "https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}" - ) - - example.add_link(name: "Job(#{Env.ci_job_name})", url: Env.ci_job_url) if Env.running_in_ci? - end + config.add_formatter(AllureRspecFormatter) + config.add_formatter(QA::Support::AllureMetadataFormatter) end end diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb index 28bae541cb8..c1df5e84f6c 100644 --- a/qa/qa/runtime/api/request.rb +++ b/qa/qa/runtime/api/request.rb @@ -6,6 +6,10 @@ module QA class Request API_VERSION = 'v4' + def self.masked_url(url) + url.sub(/private_token=.*/, "private_token=[****]") + end + def initialize(api_client, path, **query_string) query_string[:private_token] ||= api_client.personal_access_token unless query_string[:oauth_access_token] request_path = request_path(path, **query_string) @@ -13,7 +17,7 @@ module QA end def mask_url - @session_address.address.sub(/private_token=.*/, "private_token=[****]") + QA::Runtime::API::Request.masked_url(url) end def url diff --git a/qa/qa/runtime/application_settings.rb b/qa/qa/runtime/application_settings.rb index 428ed20c83f..0b2aef47576 100644 --- a/qa/qa/runtime/application_settings.rb +++ b/qa/qa/runtime/application_settings.rb @@ -26,7 +26,7 @@ module QA end def restore_application_settings(*application_settings_keys) - set_application_settings(@original_application_settings.slice(*application_settings_keys)) + set_application_settings(**@original_application_settings.slice(*application_settings_keys)) end private diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index e13061e2648..9097690de57 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -143,11 +143,12 @@ module QA if QA::Runtime::Env.remote_grid selenium_options[:url] = QA::Runtime::Env.remote_grid capabilities[:browserVersion] = 'latest' + capabilities['sauce:options'] = { tunnelIdentifier: QA::Runtime::Env.remote_tunnel_id } end Capybara::Selenium::Driver.new( app, - selenium_options + **selenium_options ) end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 5cac811d95b..a076d8db9e0 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -142,6 +142,10 @@ module QA ENV['QA_REMOTE_GRID_PROTOCOL'] || 'http' end + def remote_tunnel_id + ENV['QA_REMOTE_TUNNEL_ID'] || 'gitlab-sl_tunnel_id' + end + def browser ENV['QA_BROWSER'].nil? ? :chrome : ENV['QA_BROWSER'].to_sym end diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb index dd7f9cf898c..7011f46542b 100644 --- a/qa/qa/runtime/feature.rb +++ b/qa/qa/runtime/feature.rb @@ -32,7 +32,7 @@ module QA def enabled?(key, **scopes) feature = JSON.parse(get_features).find { |flag| flag['name'] == key.to_s } - feature && (feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], scopes)) + feature && (feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], **scopes)) end private @@ -43,7 +43,7 @@ module QA raise AuthorizationError, "Administrator access is required to enable/disable feature flags. #{e.message}" end - def enabled_scope?(gates, scopes) + def enabled_scope?(gates, **scopes) scopes.each do |key, value| case key when :project, :group, :user @@ -71,16 +71,16 @@ module QA # scopes: Any scope (user, project, group) to restrict the change to def set_and_verify(key, enable:, **scopes) msg = "#{enable ? 'En' : 'Dis'}abling feature: #{key}" - msg += " for scope \"#{scopes_to_s(scopes)}\"" if scopes.present? + msg += " for scope \"#{scopes_to_s(**scopes)}\"" if scopes.present? QA::Runtime::Logger.info(msg) Support::Retrier.retry_on_exception(sleep_interval: 2) do - set_feature(key, enable, scopes) + set_feature(key, enable, **scopes) is_enabled = nil QA::Support::Waiter.wait_until(sleep_interval: 1) do - is_enabled = enabled?(key, scopes) + is_enabled = enabled?(key, **scopes) is_enabled == enable || !enable && scopes.present? end 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 new file mode 100644 index 00000000000..6bbb859b3ee --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/bulk_import_group_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage', :requires_admin do + describe 'Bulk group import' do + let!(:staging?) { Runtime::Scenario.gitlab_address.include?('staging.gitlab.com') } + + let(:admin_api_client) { Runtime::API::Client.as_admin } + let(:user) do + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + usr.hard_delete_on_api_removal = true + end + end + + let(:api_client) { Runtime::API::Client.new(user: user) } + let(:personal_access_token) { api_client.personal_access_token } + + let(: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(:subgroup) do + Resource::Group.fabricate_via_api! do |group| + group.api_client = api_client + group.sandbox = source_group + group.path = "subgroup-for-import-#{SecureRandom.hex(4)}" + end + end + + let(:imported_subgroup) do + Resource::Group.init do |group| + group.api_client = api_client + group.sandbox = imported_group + group.path = subgroup.path + 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) unless staging? + Runtime::Feature.enable(:top_level_group_creation_enabled) if staging? + + sandbox.add_member(user, Resource::Members::AccessLevel::MAINTAINER) + + Resource::GroupLabel.fabricate_via_api! do |label| + label.api_client = api_client + label.group = source_group + label.title = "source-group-#{SecureRandom.hex(4)}" + end + Resource::GroupLabel.fabricate_via_api! do |label| + label.api_client = api_client + label.group = subgroup + label.title = "subgroup-#{SecureRandom.hex(4)}" + end + end + + # Non blocking issues: + # https://gitlab.com/gitlab-org/gitlab/-/issues/331252 + # https://gitlab.com/gitlab-org/gitlab/-/issues/333678 <- can cause 500 when creating user and group back to back + it( + 'imports group with subgroups and labels', + testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1871' + ) do + expect { imported_group.import_status }.to( + eventually_eq('finished').within(max_duration: 300, sleep_interval: 2) + ) + + aggregate_failures do + expect(imported_group.reload!).to eq(source_group) + expect(imported_group.labels).to include(*source_group.labels) + + expect(imported_subgroup.reload!).to eq(subgroup) + expect(imported_subgroup.labels).to include(*subgroup.labels) + end + end + + after do + user.remove_via_api! + ensure + Runtime::Feature.disable(:bulk_import) unless staging? + Runtime::Feature.disable(:top_level_group_creation_enabled) if staging? + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb index 1b873d35d75..72a0a761294 100644 --- a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb @@ -33,7 +33,7 @@ module QA it 'imports Github repo via api', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1858' do imported_project # import the project - expect { imported_project.reload!.import_status }.to eventually_eq('finished').within(duration: 90) + expect { imported_project.reload!.import_status }.to eventually_eq('finished').within(max_duration: 90) aggregate_failures do verify_repository_import 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 new file mode 100644 index 00000000000..385908f2176 --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb @@ -0,0 +1,362 @@ +# frozen_string_literal: true + +require 'octokit' + +# rubocop:disable Rails/Pluck +module QA + # Only executes in custom job/pipeline + RSpec.describe 'Manage', :github, :requires_admin, only: { job: 'large-github-import' } do + describe 'Project import' do + let(:logger) { Runtime::Logger.logger } + 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| + resource.api_client = api_client + end + end + + let(:github_repo) { ENV['QA_LARGE_GH_IMPORT_REPO'] || 'rspec/rspec-core' } + let(:import_max_duration) { ENV['QA_LARGE_GH_IMPORT_DURATION'] ? ENV['QA_LARGE_GH_IMPORT_DURATION'].to_i : 7200 } + let(:github_client) do + Octokit.middleware = Faraday::RackBuilder.new do |builder| + builder.response(:logger, logger, headers: false, bodies: false) + end + + Octokit::Client.new( + access_token: ENV['QA_LARGE_GH_IMPORT_GH_TOKEN'] || Runtime::Env.github_access_token, + auto_paginate: true + ) + end + + let(:gh_branches) { github_client.branches(github_repo).map(&:name) } + let(:gh_commits) { github_client.commits(github_repo).map(&:sha) } + let(:gh_repo) { github_client.repository(github_repo) } + + let(:gh_labels) do + github_client.labels(github_repo).map { |label| { name: label.name, color: "##{label.color}" } } + end + + let(:gh_milestones) do + github_client + .list_milestones(github_repo, state: 'all') + .map { |ms| { title: ms.title, description: ms.description } } + end + + let(:gh_all_issues) do + github_client.list_issues(github_repo, state: 'all') + end + + let(:gh_prs) do + gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash| + hash[pr.title] = { + body: pr.body || '', + comments: [*gh_pr_comments[pr.html_url], *gh_issue_comments[pr.html_url]].compact.sort + } + end + end + + let(:gh_issues) do + gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash| + hash[issue.title] = { + body: issue.body || '', + comments: gh_issue_comments[issue.html_url] + } + end + end + + let(:gh_issue_comments) do + github_client.issues_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| + hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key + end + end + + let(:gh_pr_comments) do + github_client.pull_requests_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash| + hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key + end + end + + let(:imported_project) do + 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.api_client = api_client + end + end + + before do + group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) + end + + after do |example| + user.remove_via_api! + next unless defined?(@import_time) + + # save data for comparison after run finished + save_json( + "data", + { + import_time: @import_time, + github: { + project_name: github_repo, + branches: gh_branches, + commits: gh_commits, + labels: gh_labels, + milestones: gh_milestones, + prs: gh_prs, + issues: gh_issues + }, + gitlab: { + project_name: imported_project.path_with_namespace, + branches: gl_branches, + commits: gl_commits, + labels: gl_labels, + milestones: gl_milestones, + mrs: mrs, + issues: gl_issues + } + }.to_json + ) + end + + it 'imports large Github repo via api' do + start = Time.now + + imported_project # import the project + fetch_github_objects # fetch all objects right after import has started + + import_status = lambda do + imported_project.reload!.import_status.tap do |status| + raise "Import of '#{imported_project.name}' failed!" if status == 'failed' + end + end + expect(import_status).to eventually_eq('finished').within(max_duration: import_max_duration, sleep_interval: 30) + @import_time = Time.now - start + + aggregate_failures do + verify_repository_import + verify_labels_import + verify_milestones_import + verify_merge_requests_import + verify_issues_import + end + end + + # Persist all objects from repository being imported + # + # @return [void] + def fetch_github_objects + logger.debug("== Fetching objects for github repo: '#{github_repo}' ==") + + gh_repo + gh_branches + gh_commits + gh_prs + gh_issues + gh_labels + gh_milestones + end + + # Verify repository imported correctly + # + # @return [void] + def verify_repository_import + logger.debug("== Verifying repository import ==") + expect(imported_project.description).to eq(gh_repo.description) + # check via include, importer creates more branches + # https://gitlab.com/gitlab-org/gitlab/-/issues/332711 + expect(gl_branches).to include(*gh_branches) + expect(gl_commits).to match_array(gh_commits) + end + + # Verify imported merge requests and mr issues + # + # @return [void] + def verify_merge_requests_import + logger.debug("== Verifying merge request import ==") + verify_mrs_or_issues('mr') + end + + # Verify imported issues and issue comments + # + # @return [void] + def verify_issues_import + logger.debug("== Verifying issue import ==") + verify_mrs_or_issues('issue') + end + + # Verify imported labels + # + # @return [void] + def verify_labels_import + logger.debug("== Verifying label import ==") + # check via include, additional labels can be inherited from parent group + expect(gl_labels).to include(*gh_labels) + end + + # Verify milestones import + # + # @return [void] + def verify_milestones_import + logger.debug("== Verifying milestones import ==") + expect(gl_milestones).to match_array(gh_milestones) + end + + private + + # Verify imported mrs or issues + # + # @param [String] type verification object, 'mrs' or 'issues' + # @return [void] + def verify_mrs_or_issues(type) + msg = ->(title) { "expected #{type} with title '#{title}' to have" } + expected = type == 'mr' ? mrs : gl_issues + actual = type == 'mr' ? gh_prs : gh_issues + + # Compare length to have easy to read overview how many objects are missing + expect(expected.length).to( + eq(actual.length), + "Expected to contain same amount of #{type}s. Expected: #{expected.length}, actual: #{actual.length}" + ) + logger.debug("= Comparing #{type}s =") + actual.each do |title, actual_item| + print "." # indicate that it is still going but don't spam the output with newlines + + expected_item = expected[title] + + expect(expected_item).to be_truthy, "#{msg.call(title)} been imported" + next unless expected_item + + expect(expected_item[:body]).to( + include(actual_item[:body]), + "#{msg.call(title)} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])}" + ) + expect(expected_item[:comments].length).to( + eq(actual_item[:comments].length), + "#{msg.call(title)} same amount of comments" + ) + expect(expected_item[:comments]).to match_array(actual_item[:comments]) + end + puts # print newline after last print to make output pretty + end + + # Imported project branches + # + # @return [Array] + def gl_branches + @gl_branches ||= begin + logger.debug("= Fetching branches =") + imported_project.repository_branches(auto_paginate: true).map { |b| b[:name] } + end + end + + # Imported project commits + # + # @return [Array] + def gl_commits + @gl_commits ||= begin + logger.debug("= Fetching commits =") + imported_project.commits(auto_paginate: true, attempts: 2).map { |c| c[:id] } + end + end + + # Imported project labels + # + # @return [Array] + def gl_labels + @gl_labels ||= begin + logger.debug("= Fetching labels =") + imported_project.labels(auto_paginate: true).map { |label| label.slice(:name, :color) } + end + end + + # Imported project milestones + # + # @return [<Type>] <description> + def gl_milestones + @gl_milestones ||= begin + logger.debug("= Fetching milestones =") + imported_project.milestones(auto_paginate: true).map { |ms| ms.slice(:title, :description) } + end + end + + # Imported project merge requests + # + # @return [Hash] + def mrs + @mrs ||= begin + logger.debug("= Fetching merge requests =") + imported_mrs = imported_project.merge_requests(auto_paginate: true, attempts: 2) + logger.debug("= Transforming merge request objects for comparison =") + imported_mrs.each_with_object({}) do |mr, hash| + resource = Resource::MergeRequest.init do |resource| + resource.project = imported_project + resource.iid = mr[:iid] + resource.api_client = api_client + end + + hash[mr[:title]] = { + body: mr[:description], + comments: resource.comments(auto_paginate: true, attempts: 2) + # remove system notes + .reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) } + .map { |c| sanitize(c[:body]) } + } + end + end + end + + # Imported project issues + # + # @return [Hash] + def gl_issues + @gl_issues ||= begin + logger.debug("= Fetching issues =") + imported_issues = imported_project.issues(auto_paginate: true, attempts: 2) + logger.debug("= Transforming issue objects for comparison =") + imported_issues.each_with_object({}) do |issue, hash| + resource = Resource::Issue.init do |issue_resource| + issue_resource.project = imported_project + issue_resource.iid = issue[:iid] + issue_resource.api_client = api_client + end + + hash[issue[:title]] = { + body: issue[:description], + comments: resource.comments(auto_paginate: true, attempts: 2).map { |c| sanitize(c[:body]) } + } + end + end + end + + # Remove added prefixes by importer + # + # @param [String] body + # @return [String] + def sanitize(body) + body.gsub(/\*Created by: \S+\*\n\n/, "") + end + + # Save json as file + # + # @param [String] name + # @param [String] json + # @return [void] + def save_json(name, json) + File.open("tmp/#{name}.json", "w") { |file| file.write(json) } + end + end + end +end +# rubocop:enable Rails/Pluck diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb index 799efc243d4..9ac27a2ca06 100644 --- a/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb +++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb @@ -25,18 +25,6 @@ module QA push.file_content = "Target branch test target branch #{SecureRandom.hex(8)}" end - # Confirm the target branch can be checked out to avoid a race condition - # where the subsequent push option attempts to create an MR before the target branch is ready. - Support::Retrier.retry_on_exception(sleep_interval: 5) do - Git::Repository.perform do |repository| - repository.uri = project.repository_http_location.uri - repository.use_default_credentials - repository.clone - repository.configure_identity('GitLab QA', 'root@gitlab.com') - repository.checkout(target_branch) - end - end - Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.branch_name = "push-options-test-#{SecureRandom.hex(8)}" 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 4bd99b4820e..c65d981d99a 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 @@ -47,7 +47,7 @@ module QA def create_project(user, api_client, project_name) project = Resource::Project.fabricate_via_api! do |project| - project.standalone = true + project.personal_namespace = true project.add_name_uuid = false project.name = project_name project.path_with_namespace = "#{user.username}/#{project_name}" diff --git a/qa/qa/specs/features/api/5_package/container_registry_spec.rb b/qa/qa/specs/features/api/5_package/container_registry_spec.rb index 5003d49fe6c..f79a3ebbe03 100644 --- a/qa/qa/specs/features/api/5_package/container_registry_spec.rb +++ b/qa/qa/specs/features/api/5_package/container_registry_spec.rb @@ -3,7 +3,7 @@ require 'airborne' module QA - RSpec.describe 'Package', only: { subdomain: :staging } do + RSpec.describe 'Package', only: { subdomain: %i[staging pre] } do include Support::Api describe 'Container Registry' do @@ -13,6 +13,7 @@ module QA Resource::Project.fabricate_via_api! do |project| project.name = 'project-with-registry-api' project.template_name = 'express' + project.api_client = api_client end end @@ -37,6 +38,12 @@ module QA - docker:19.03.12-dind variables: IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + DOCKER_HOST: tcp://docker:2376 + DOCKER_TLS_CERTDIR: "/certs" + DOCKER_TLS_VERIFY: 1 + DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client" + before_script: + - until docker info; do sleep 1; done script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $IMAGE_TAG . @@ -50,6 +57,7 @@ module QA MEDIA_TYPE: 'application/vnd.docker.distribution.manifest.v2+json' before_script: - token=$(curl -u "$CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD" "https://$CI_SERVER_HOST/jwt/auth?service=container_registry&scope=repository:$CI_PROJECT_PATH:pull,push,delete" | jq -r '.token') + - echo $token script: - 'digest=$(curl -L -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/master" | jq -r ".layers[0].digest")' - 'curl -L -X DELETE -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/blobs/$digest"' @@ -57,7 +65,6 @@ module QA - 'digest=$(curl -L -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/master" | jq -r ".config.digest")' - 'curl -L -X DELETE -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/$digest"' - 'curl -L --head -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/$digest"' - YAML end @@ -67,8 +74,9 @@ module QA it 'pushes, pulls image to the registry and deletes image blob, manifest and tag', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1738' do Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project + commit.api_client = api_client commit.commit_message = 'Add .gitlab-ci.yml' + commit.project = project commit.add_files([{ file_path: '.gitlab-ci.yml', content: gitlab_ci_yaml @@ -77,7 +85,7 @@ module QA Support::Waiter.wait_until(max_duration: 10) { pipeline_is_triggered? } - Support::Retrier.retry_until(max_duration: 260, sleep_interval: 5) do + Support::Retrier.retry_until(max_duration: 300, sleep_interval: 5) do latest_pipeline_succeed? 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 6c2ff005f49..fe17b5c34e1 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 @@ -29,27 +29,11 @@ module QA end end - let(:subgroup) do - Resource::Group.fabricate_via_api! do |group| - group.api_client = api_client - group.sandbox = source_group - group.path = "subgroup-for-import-#{SecureRandom.hex(4)}" - end - end - let(:imported_group) do - Resource::Group.init do |group| + Resource::BulkImportGroup.init do |group| group.api_client = api_client group.sandbox = sandbox - group.path = source_group.path - end - end - - let(:imported_subgroup) do - Resource::Group.init do |group| - group.api_client = api_client - group.sandbox = imported_group - group.path = subgroup.path + group.source_group_path = source_group.path end end @@ -61,7 +45,6 @@ module QA # create groups explicitly before connecting gitlab instance source_group - subgroup Flow::Login.sign_in(as: user) Page::Main::Menu.perform(&:go_to_create_group) @@ -74,33 +57,15 @@ module QA # Non blocking issues: # https://gitlab.com/gitlab-org/gitlab/-/issues/331252 # https://gitlab.com/gitlab-org/gitlab/-/issues/333678 <- can cause 500 when creating user and group back to back - it( - 'imports group with subgroups and labels', - testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785' - ) do - Resource::GroupLabel.fabricate_via_api! do |label| - label.api_client = api_client - label.group = source_group - label.title = "source-group-#{SecureRandom.hex(4)}" - end - Resource::GroupLabel.fabricate_via_api! do |label| - label.api_client = api_client - label.group = subgroup - label.title = "subgroup-#{SecureRandom.hex(4)}" - end - + it 'imports group from UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1785' do Page::Group::BulkImport.perform do |import_page| - import_page.import_group(source_group.path, sandbox.path) - - expect(import_page).to have_imported_group(source_group.path, wait: 180) + import_page.import_group(imported_group.path, imported_group.sandbox.path) - aggregate_failures do - expect { imported_group.reload! }.to eventually_eq(source_group).within(duration: 10) - expect { imported_group.labels }.to eventually_include(*source_group.labels).within(duration: 10) + expect(import_page).to have_imported_group(imported_group.path, wait: 300) - # Do not validate subgroups until https://gitlab.com/gitlab-org/gitlab/-/issues/332818 is resolved - # expect { imported_subgroup.reload! }.to eventually_eq(subgroup).within(duration: 30) - # expect { imported_subgroup.labels }.to eventually_include(*subgroup.labels).within(duration: 30) + imported_group.reload!.visit! + Page::Group::Show.perform do |group| + expect(group).to have_content(imported_group.path) end end end 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 4fffc786c14..564b14a872f 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,24 +2,52 @@ module QA RSpec.describe 'Manage', :smoke do - describe 'Project creation' do - it 'user creates a new project', - testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1857' do + describe 'Project' do + shared_examples 'successful project creation' do + it 'creates a new project' do + Page::Project::Show.perform do |project| + expect(project).to have_content(project_name) + expect(project).to have_content( + /Project \S?#{project_name}\S+ was successfully created/ + ) + expect(project).to have_content('create awesome project test') + expect(project).to have_content('The repository for this project is empty') + end + end + end + + before do Flow::Login.sign_in + project + end - created_project = Resource::Project.fabricate_via_browser_ui! do |project| - project.name = 'awesome-project' - project.description = 'create awesome project test' + context 'in group', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1857' do + let(:project_name) { "project-in-group-#{SecureRandom.hex(8)}" } + let(:project) do + Resource::Project.fabricate_via_browser_ui! do |project| + project.name = project_name + project.description = 'create awesome project test' + end end - Page::Project::Show.perform do |project| - expect(project).to have_content(created_project.name) - expect(project).to have_content( - /Project \S?awesome-project\S+ was successfully created/ - ) - expect(project).to have_content('create awesome project test') - expect(project).to have_content('The repository for this project is empty') + it_behaves_like 'successful project creation' + end + + context 'in personal namespace', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1888' do + let(:project_name) { "project-in-personal-namespace-#{SecureRandom.hex(8)}" } + let(:project) do + Resource::Project.fabricate_via_browser_ui! do |project| + project.name = project_name + project.description = 'create awesome project test' + project.personal_namespace = true + end end + + it_behaves_like 'successful project creation' + end + + after do + project.remove_via_api! end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index 4f85fa257a2..c55ecb28361 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -3,9 +3,11 @@ module QA RSpec.describe 'Manage', :github, :requires_admin do describe 'Project import' do - let!(:api_client) { Runtime::API::Client.as_admin } - let!(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } } - let!(:user) do + let(:github_repo) { 'gitlab-qa-github/test-project' } + let(:imported_project_name) { 'imported-project' } + let(:api_client) { Runtime::API::Client.as_admin } + let(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } } + let(:user) do Resource::User.fabricate_via_api! do |resource| resource.api_client = api_client resource.hard_delete_on_api_removal = true @@ -13,16 +15,25 @@ module QA end let(:imported_project) do - Resource::ProjectImportedFromGithub.fabricate_via_browser_ui! do |project| - project.name = 'imported-project' + Resource::ProjectImportedFromGithub.init do |project| + project.import = true + project.add_name_uuid = false + project.name = imported_project_name project.group = group project.github_personal_access_token = Runtime::Env.github_access_token - project.github_repository_path = 'gitlab-qa-github/test-project' + project.github_repository_path = github_repo end end before do group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) + + Flow::Login.sign_in(as: user) + Page::Main::Menu.perform(&:go_to_create_project) + Page::Project::New.perform do |project_page| + project_page.click_import_project + project_page.click_github_link + end end after do @@ -30,13 +41,24 @@ module QA end it 'imports a GitHub repo', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1762' do - Flow::Login.sign_in(as: user) + Page::Project::Import::Github.perform do |import_page| + import_page.add_personal_access_token(Runtime::Env.github_access_token) + import_page.import!(github_repo, group.full_path, imported_project_name) - imported_project # import the project + aggregate_failures do + expect(import_page).to have_imported_project(github_repo) + # validate button is present instead of navigating to avoid dealing with multiple tabs + # which makes the test more complicated + expect(import_page).to have_go_to_project_button(github_repo) + end + end + imported_project.reload!.visit! Page::Project::Show.perform do |project| - expect(project).to have_content(imported_project.name) - expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.') + aggregate_failures do + expect(project).to have_content(imported_project_name) + expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.') + end end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb index 59d34612ca7..dfdc9b7c9b4 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/331978', type: :bug } do + RSpec.describe 'Create' do context 'Design Management' do let(:design) do Resource::Design.fabricate! do |design| diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index d11afde5648..37008e6d507 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -15,7 +15,7 @@ module QA merge_request.visit! end - it 'views the merge request email patches', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1689' do + it 'views the merge request email patches', :can_use_large_setup, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1689' do Page::MergeRequest::Show.perform(&:view_email_patches) expect(page.text).to start_with('From') @@ -23,10 +23,11 @@ module QA expect(page).to have_content("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}") end - it 'views the merge request plain diff', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/417' do + it 'views the merge request plain diff', :can_use_large_setup, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/417' do Page::MergeRequest::Show.perform(&:view_plain_diff) - expect(page.text).to start_with("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}") + expect(page.text).to start_with('diff') + expect(page).to have_content("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}") expect(page).to have_content('+File Added') end end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb new file mode 100644 index 00000000000..2a46604f8ac --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + context 'Content Editor' do + let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! } + let(:page_title) { 'Content Editor Page' } + let(:heading_text) { 'My New Heading' } + let(:image_file_name) { 'testfile.png' } + + before do + Flow::Login.sign_in + end + + after do + initial_wiki.project.remove_via_api! + end + + it 'creates a formatted Wiki page with an image uploaded', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1861' do + initial_wiki.visit! + + Page::Project::Wiki::Show.perform(&:click_new_page) + + Page::Project::Wiki::Edit.perform do |edit| + edit.set_title(page_title) + edit.use_new_editor + edit.add_heading('Heading 1', heading_text) + edit.upload_image(File.absolute_path(File.join('qa', 'fixtures', 'designs', image_file_name))) + end + + Page::Project::Wiki::Edit.perform(&:click_submit) + + Page::Project::Wiki::Show.perform do |wiki| + aggregate_failures 'page shows expected content' do + expect(wiki).to have_title(page_title) + expect(wiki).to have_heading('h1', heading_text) + expect(wiki).to have_image(image_file_name) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb index 5083b7b0859..5b3949f9c3a 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb @@ -1,10 +1,8 @@ # frozen_string_literal: true module QA - RSpec.describe 'Verify', :requires_admin do + RSpec.describe 'Verify' do describe 'Include local config file paths with wildcard' do - let(:feature_flag) { :ci_wildcard_file_paths } - let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'project-with-pipeline' @@ -12,7 +10,6 @@ module QA end before do - Runtime::Feature.enable(feature_flag) Flow::Login.sign_in add_files_to_project project.visit! @@ -20,7 +17,6 @@ module QA end after do - Runtime::Feature.disable(feature_flag) project.remove_via_api! end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb index 01aada2d6dd..b43581289ef 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/merge_mr_when_pipline_is_blocked_spec.rb @@ -73,7 +73,7 @@ module QA it 'can still merge MR successfully', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/971' do Page::MergeRequest::Show.perform do |show| # waiting for manual action status shows status badge 'blocked' on pipelines page - show.wait_until(reload: false) { show.has_pipeline_status?('waiting for manual action') } + show.has_pipeline_status?('waiting for manual action') show.merge! expect(show).to be_merged diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb new file mode 100644 index 00000000000..47b36b55c8c --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/mr_event_rule_pipeline_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +require 'faker' + +module QA + RSpec.describe 'Verify', :runner do + context 'When job is configured to only run on merge_request_events' do + let(:mr_only_job_name) { 'mr_only_job' } + let(:non_mr_only_job_name) { 'non_mr_only_job' } + let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'merge-request-only-job' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + let!(:ci_file) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: <<~YAML + #{mr_only_job_name}: + script: echo 'OK' + rules: + - if: '$CI_PIPELINE_SOURCE == "merge_request_event"' + #{non_mr_only_job_name}: + script: echo 'OK' + rules: + - if: '$CI_PIPELINE_SOURCE != "merge_request_event"' + YAML + } + ] + ) + end + end + + let(:merge_request) do + Resource::MergeRequest.fabricate_via_api! do |merge_request| + merge_request.project = project + merge_request.description = Faker::Lorem.sentence + merge_request.target_new_branch = false + merge_request.file_name = 'new.txt' + merge_request.file_content = Faker::Lorem.sentence + end + end + + before do + Flow::Login.sign_in + merge_request.visit! + Page::MergeRequest::Show.perform(&:click_pipeline_link) + end + + after do + runner.remove_via_api! + project.remove_via_api! + end + + it 'only runs the job configured to run on merge requests', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/170' do + Page::Project::Pipeline::Show.perform do |pipeline| + aggregate_failures do + expect(pipeline).to have_job(mr_only_job_name) + expect(pipeline).to have_no_job(non_mr_only_job_name) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb index c1625f1e679..adacedb36ab 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb @@ -41,7 +41,7 @@ module QA after do runner.remove_via_api! - group.remove_via_api! + [upstream_project, downstream_project].each(&:remove_via_api!) end it 'runs the pipeline with composed config', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1086' do diff --git a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb index 07484feb686..7d3f8f2b1d4 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb @@ -39,9 +39,7 @@ module QA merge_request.visit! Page::MergeRequest::Show.perform do |mr_widget| - Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 5) do - mr_widget.has_pipeline_status?('passed') - end + mr_widget.has_pipeline_status?('passed') expect(mr_widget).to have_content('Test coverage 66.67%') end end diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb index 4b7669810ec..375a371c2b1 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry_omnibus_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :registry, :orchestrated do + RSpec.describe 'Package', :registry, :orchestrated, only: { pipeline: :main } do describe 'Self-managed Container Registry' do let(:project) do Resource::Project.fabricate_via_api! do |project| 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 new file mode 100644 index 00000000000..be1d0dd8e81 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Package', :orchestrated, :registry do + describe 'Dependency Proxy' do + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'dependency-proxy-project' + project.visibility = :private + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.name = "qa-runner-#{Time.now.to_i}" + runner.tags = ["runner-for-#{project.name}"] + runner.executor = :docker + runner.project = project + end + end + + let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) } + let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" } + let(:dependency_proxy_url) { "#{gitlab_host_with_port}/#{project.group.full_path}/dependency_proxy/containers" } + + before do + Flow::Login.sign_in + + project.group.visit! + + Page::Group::Menu.perform(&:go_to_dependency_proxy) + + Page::Group::DependencyProxy.perform do |index| + expect(index).to have_dependency_proxy_enabled + end + end + + after do + runner.remove_via_api! + end + + where(:docker_client_version) do + %w[docker:19.03.12 docker:20.10] + end + + with_them do + it "pulls an image using the dependency proxy", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1862' do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files([{ + file_path: '.gitlab-ci.yml', + content: + <<~YAML + dependency-proxy-pull-test: + image: "#{docker_client_version}" + services: + - name: "#{docker_client_version}-dind" + command: + - /bin/sh + - -c + - | + apk add --no-cache openssl + true | openssl s_client -showcerts -connect gitlab.test:5050 > /usr/local/share/ca-certificates/gitlab.test.crt + update-ca-certificates + dockerd-entrypoint.sh || exit + before_script: + - apk add curl jq grep + - docker login -u "$CI_DEPENDENCY_PROXY_USER" -p "$CI_DEPENDENCY_PROXY_PASSWORD" "$CI_DEPENDENCY_PROXY_SERVER" + script: + - docker pull #{dependency_proxy_url}/alpine:latest + - TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq --raw-output .token) + - 'curl --head --header "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" 2>&1' + - docker pull #{dependency_proxy_url}/alpine:latest + - 'curl --head --header "Authorization: Bearer $TOKEN" "https://registry-1.docker.io/v2/ratelimitpreview/test/manifests/latest" 2>&1' + tags: + - "runner-for-#{project.name}" + YAML + }]) + end + + project.visit! + Flow::Pipeline.visit_latest_pipeline + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('dependency-proxy-pull-test') + end + + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 800) + end + + project.group.visit! + + Page::Group::Menu.perform(&:go_to_dependency_proxy) + + Page::Group::DependencyProxy.perform do |index| + expect(index).to have_blob_count("Contains 2 blobs of images") + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb b/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb index 65fc12545b7..8c686e65e33 100644 --- a/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/online_garbage_collection_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Package' do - describe 'Container Registry Online Garbage Collection', :registry_gc, only: { subdomain: %i[pre] } do + describe 'Container Registry Online Garbage Collection', :registry_gc, only: { subdomain: %i[pre] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/337791', type: :waiting_on } do let(:group) { Resource::Group.fabricate_via_api! } let(:imported_project) do diff --git a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb index a9034174cab..7b924f1b52b 100644 --- a/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/pypi_repository_spec.rb @@ -26,10 +26,10 @@ module QA end end - let(:gitlab_address_with_port) do - uri = URI.parse(Runtime::Scenario.gitlab_address) - "#{uri.scheme}://#{uri.host}:#{uri.port}" - end + let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) } + let(:gitlab_address_with_port) { "#{uri.scheme}://#{uri.host}:#{uri.port}" } + let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" } + let(:personal_access_token) { Runtime::Env.personal_access_token } before do Flow::Login.sign_in @@ -42,14 +42,25 @@ module QA content: <<~YAML image: python:latest + stages: + - run + - install run: + stage: run script: - pip install twine - python setup.py sdist bdist_wheel - "TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*" tags: - "runner-for-#{project.name}" + install: + stage: install + script: + - "pip install mypypipackage --no-deps --index-url #{uri.scheme}://#{personal_access_token}:#{personal_access_token}@#{gitlab_host_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host #{gitlab_host_with_port}" + tags: + - "runner-for-#{project.name}" + YAML }, { @@ -87,6 +98,16 @@ module QA Page::Project::Job::Show.perform do |job| expect(job).to be_successful(timeout: 800) end + + Flow::Pipeline.visit_latest_pipeline + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('install') + end + + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 800) + end end after do @@ -95,20 +116,22 @@ module QA project&.remove_via_api! end - it 'publishes a pypi package and deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1087' do - Page::Project::Menu.perform(&:click_packages_link) + context 'when at the project level' do + it 'publishes and installs a pypi package and deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1087' do + 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::Index.perform do |index| + expect(index).to have_package(package.name) + index.click_package(package.name) + end - Page::Project::Packages::Show.perform(&:click_delete) + Page::Project::Packages::Show.perform(&:click_delete) - Page::Project::Packages::Index.perform do |index| - aggregate_failures do - expect(index).to have_content("Package deleted successfully") - expect(index).not_to have_package(package.name) + Page::Project::Packages::Index.perform do |index| + aggregate_failures do + expect(index).to have_content("Package deleted successfully") + expect(index).not_to have_package(package.name) + end end end end diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 8e61ec4d2d7..f4a5c715684 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -5,27 +5,31 @@ require 'digest/sha1' module QA RSpec.describe 'Release', :runner do describe 'Git clone using a deploy key' do - before do - Flow::Login.sign_in - - @runner_name = "qa-runner-#{Time.now.to_i}" + let(:runner_name) { "qa-runner-#{SecureRandom.hex(4)}" } + let(:repository_location) { project.repository_ssh_location } - @project = Resource::Project.fabricate_via_api! do |project| + let(:project) do + Resource::Project.fabricate_via_api! do |project| project.name = 'deploy-key-clone-project' end + end - @repository_location = @project.repository_ssh_location - - @runner = Resource::Runner.fabricate_via_api! do |resource| - resource.project = @project - resource.name = @runner_name - resource.tags = %w[qa docker] + let!(:runner) do + Resource::Runner.fabricate_via_api! do |resource| + resource.project = project + resource.name = runner_name + resource.tags = [runner_name] resource.image = 'gitlab/gitlab-runner:alpine' end end + before do + Flow::Login.sign_in + end + after do - @runner.remove_via_api! + runner.remove_via_api! + project.remove_via_api! end keys = [ @@ -39,7 +43,7 @@ module QA key = key_class.new(*bits) Resource::DeployKey.fabricate_via_browser_ui! do |resource| - resource.project = @project + resource.project = project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key end @@ -49,25 +53,23 @@ module QA make_ci_variable(deploy_key_name, key) gitlab_ci = <<~YAML - cat-config: - script: - - apk add --update --no-cache openssh-client - - mkdir -p ~/.ssh - - ssh-keyscan -p #{@repository_location.port} #{@repository_location.host} >> ~/.ssh/known_hosts - - eval $(ssh-agent -s) - - ssh-add -D - - echo "$#{deploy_key_name}" | ssh-add - - - git clone #{@repository_location.git_uri} - - cd #{@project.name} - - git checkout #{deploy_key_name} - - sha1sum .gitlab-ci.yml - tags: - - qa - - docker + cat-config: + script: + - which ssh-agent || ( apk --update add openssh-client ) + - mkdir -p ~/.ssh + - ssh-keyscan -p #{repository_location.port} #{repository_location.host} >> ~/.ssh/known_hosts + - eval $(ssh-agent -s) + - ssh-add -D + - echo "$#{deploy_key_name}" | ssh-add - + - git clone #{repository_location.git_uri} + - cd #{project.name} + - git checkout #{deploy_key_name} + - sha1sum .gitlab-ci.yml + tags: [#{runner_name}] YAML Resource::Repository::ProjectPush.fabricate! do |resource| - resource.project = @project + resource.project = project resource.file_name = '.gitlab-ci.yml' resource.commit_message = 'Add .gitlab-ci.yml' resource.file_content = gitlab_ci @@ -81,8 +83,10 @@ module QA Page::Project::Pipeline::Show.perform(&:click_on_first_job) Page::Project::Job::Show.perform do |job| - expect(job).to be_successful - expect(job.output).to include(sha1sum) + aggregate_failures 'job succeeds and has correct sha1sum' do + expect(job).to be_successful + expect(job.output).to include(sha1sum) + end end end @@ -90,7 +94,7 @@ module QA def make_ci_variable(key_name, key) Resource::CiVariable.fabricate_via_api! do |resource| - resource.project = @project + resource.project = project resource.key = key_name resource.value = key.private_key resource.masked = false 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 1803b4b16de..3a59efe645a 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 @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - RSpec.describe 'Configure' do - describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225315', type: :flaky } do + RSpec.describe 'Configure', except: { job: 'review-qa-*' } do + describe 'Kubernetes Cluster Integration', :requires_admin, :skip_live_env, :smoke do context 'Project Clusters' do let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! } let(:project) do @@ -20,7 +20,7 @@ module QA cluster.remove! end - it 'can create and associate a project cluster', :smoke, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/707' do + it 'can create and associate a project cluster', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/707' do Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster| k8s_cluster.project = project k8s_cluster.cluster = cluster diff --git a/qa/qa/specs/helpers/context_formatter.rb b/qa/qa/specs/helpers/context_formatter.rb new file mode 100644 index 00000000000..26db7c3b67e --- /dev/null +++ b/qa/qa/specs/helpers/context_formatter.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +require 'rspec/core' +require "rspec/core/formatters/base_formatter" + +module QA + module Specs + module Helpers + class ContextFormatter < ::RSpec::Core::Formatters::BaseFormatter + include ContextSelector + + ::RSpec::Core::Formatters.register( + self, + :example_group_started, + :example_started + ) + + # Starts example group + # @param [RSpec::Core::Notifications::GroupNotification] example_group_notification + # @return [void] + def example_group_started(example_group_notification) + set_skip_metadata(example_group_notification.group) + end + + # Starts example + # @param [RSpec::Core::Notifications::ExampleNotification] example_notification + # @return [void] + def example_started(example_notification) + example = example_notification.example + + # if skip propagated from example_group, do not reset skip metadata + set_skip_metadata(example_notification.example) unless example.metadata[:skip] + end + + private + + # Skip example_group or example + # + # @param [<RSpec::Core::ExampleGroup, RSpec::Core::Example>] example + # @return [void] + def set_skip_metadata(example) + return skip_only(example.metadata) if example.metadata.key?(:only) + return skip_except(example.metadata) if example.metadata.key?(:except) + end + + # Skip based on 'only' condition + # + # @param [Hash] metadata + # @return [void] + def skip_only(metadata) + return if context_matches?(metadata[:only]) + + metadata[:skip] = 'Test is not compatible with this environment or pipeline' + end + + # Skip based on 'except' condition + # + # @param [Hash] metadata + # @return [void] + def skip_except(metadata) + return unless except?(metadata[:except]) + + metadata[:skip] = 'Test is excluded in this job' + end + end + end + end +end diff --git a/qa/qa/specs/helpers/context_selector.rb b/qa/qa/specs/helpers/context_selector.rb index 40ecb9b3506..57665babf68 100644 --- a/qa/qa/specs/helpers/context_selector.rb +++ b/qa/qa/specs/helpers/context_selector.rb @@ -8,18 +8,6 @@ module QA module ContextSelector extend self - def configure_rspec - ::RSpec.configure do |config| - config.before do |example| - if example.metadata.key?(:only) - skip('Test is not compatible with this environment or pipeline') unless ContextSelector.context_matches?(example.metadata[:only]) - elsif example.metadata.key?(:except) - skip('Test is excluded in this job') if ContextSelector.except?(example.metadata[:except]) - end - end - end - end - def except?(*options) return false if Runtime::Env.ci_job_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:job].present? } return false if Runtime::Env.ci_project_name.blank? && options.any? { |o| o.is_a?(Hash) && o[:pipeline].present? } diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb index 15b4ed8336b..49d91fc87cd 100644 --- a/qa/qa/specs/helpers/quarantine.rb +++ b/qa/qa/specs/helpers/quarantine.rb @@ -10,26 +10,6 @@ module QA extend self - def configure_rspec - ::RSpec.configure do |config| - config.before(:context, :quarantine) do - Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class) - end - - config.before do |example| - Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example) - end - end - end - - # Skip the entire context if a context is quarantined. This avoids running - # before blocks unnecessarily. - def skip_or_run_quarantined_contexts(filters, example) - return unless example.metadata.key?(:quarantine) - - skip_or_run_quarantined_tests_or_contexts(filters, example) - end - # Skip tests in quarantine unless we explicitly focus on them. def skip_or_run_quarantined_tests_or_contexts(filters, example) if filters.key?(:quarantine) @@ -43,19 +23,19 @@ module QA # running that ldap test as well because of the :quarantine metadata. # We could use an exclusion filter, but this way the test report will list # the quarantined tests when they're not run so that we're aware of them - skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters) - else - if example.metadata.key?(:quarantine) - quarantine_tag = example.metadata[:quarantine] - - if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only) - # If the :quarantine hash contains :only, we respect that. - # For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging. - return unless ContextSelector.context_matches?(quarantine_tag[:only]) - end + if should_skip_when_focused?(example.metadata, included_filters) + example.metadata[:skip] = "Only running tests tagged with :quarantine and any of #{included_filters.keys}" + end + elsif example.metadata.key?(:quarantine) + quarantine_tag = example.metadata[:quarantine] - skip(quarantine_message(quarantine_tag)) + if quarantine_tag.is_a?(Hash) && quarantine_tag&.key?(:only) && !ContextSelector.context_matches?(quarantine_tag[:only]) + # If the :quarantine hash contains :only, we respect that. + # For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging. + return end + + example.metadata[:skip] = quarantine_message(quarantine_tag) end end @@ -64,7 +44,7 @@ module QA end def quarantine_message(quarantine_tag) - quarantine_message = %w(In quarantine) + quarantine_message = %w[In quarantine] quarantine_message << case quarantine_tag when String ": #{quarantine_tag}" diff --git a/qa/qa/specs/helpers/quarantine_formatter.rb b/qa/qa/specs/helpers/quarantine_formatter.rb new file mode 100644 index 00000000000..c42debee07c --- /dev/null +++ b/qa/qa/specs/helpers/quarantine_formatter.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'rspec/core' +require "rspec/core/formatters/base_formatter" + +module QA + module Specs + module Helpers + class QuarantineFormatter < ::RSpec::Core::Formatters::BaseFormatter + include Quarantine + + ::RSpec::Core::Formatters.register( + self, + :example_group_started, + :example_started + ) + + # Starts example group + # @param [RSpec::Core::Notifications::GroupNotification] example_group_notification + # @return [void] + def example_group_started(example_group_notification) + group = example_group_notification.group + + skip_or_run_quarantined_tests_or_contexts(filters, group) + end + + # Starts example + # @param [RSpec::Core::Notifications::ExampleNotification] example_notification + # @return [void] + def example_started(example_notification) + example = example_notification.example + + # if skip propagated from example_group, do not reset skip metadata + skip_or_run_quarantined_tests_or_contexts(filters, example) unless example.metadata[:skip] + end + + private + + def filters + @filters ||= ::RSpec.configuration.inclusion_filter.rules + end + end + end + end +end diff --git a/qa/qa/specs/helpers/rspec.rb b/qa/qa/specs/helpers/rspec.rb index f49e556b0d9..853dfbfd1b6 100644 --- a/qa/qa/specs/helpers/rspec.rb +++ b/qa/qa/specs/helpers/rspec.rb @@ -19,8 +19,10 @@ module QA # expanding into the global state # See: https://github.com/rspec/rspec-core/issues/2603 def describe_successfully(*args, &describe_body) + reporter = ::RSpec.configuration.reporter + example_group = RSpec.describe(*args, &describe_body) - ran_successfully = example_group.run RaiseOnFailuresReporter + ran_successfully = example_group.run reporter expect(ran_successfully).to eq true example_group end diff --git a/qa/qa/support/allure_metadata_formatter.rb b/qa/qa/support/allure_metadata_formatter.rb new file mode 100644 index 00000000000..8a18eeca839 --- /dev/null +++ b/qa/qa/support/allure_metadata_formatter.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'rspec/core' +require "rspec/core/formatters/base_formatter" + +module QA + module Support + class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter + ::RSpec::Core::Formatters.register( + self, + :example_started + ) + + # Starts example + # @param [RSpec::Core::Notifications::ExampleNotification] example_notification + # @return [void] + def example_started(example_notification) + example = example_notification.example + + testcase = example.metadata[:testcase] + example.tms('Testcase', testcase) if testcase + + quarantine_issue = example.metadata.dig(:quarantine, :issue) + example.issue('Quarantine issue', quarantine_issue) if quarantine_issue + + spec_file = example.file_path.split('/').last + example.issue( + 'Failure issues', + "https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}" + ) + return unless Runtime::Env.running_in_ci? + + example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url) + end + end + end +end diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index de9da3171b0..579227b4f7a 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -79,11 +79,31 @@ module QA error.response end - def with_paginated_response_body(url) + def auto_paginated_response(url, attempts: 0) + pages = [] + with_paginated_response_body(url, attempts: attempts) { |response| pages << response } + + pages.flatten + end + + def with_paginated_response_body(url, attempts: 0) + not_ok_error = lambda do |resp| + raise "Failed to GET #{QA::Runtime::API::Request.masked_url(url)} - (#{resp.code}): `#{resp}`." + end + loop do - response = get(url) + response = if attempts > 0 + Retrier.retry_on_exception(max_attempts: attempts, log: false) do + get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK } + end + else + get(url).tap { |resp| not_ok_error.call(resp) if resp.code != HTTP_STATUS_OK } + end + + page, pages = response.headers.values_at(:x_page, :x_total_pages) + api_endpoint = url.match(%r{v4/(\S+)\?})[1] - QA::Runtime::Logger.debug("Fetching page #{response.headers[:x_page]} of #{response.headers[:x_total_pages]}...") + QA::Runtime::Logger.debug("Fetching page (#{page}/#{pages}) for '#{api_endpoint}' ...") unless pages.to_i <= 1 yield parse_body(response) @@ -95,8 +115,11 @@ module QA end def pagination_links(response) - response.headers[:link].split(',').map do |link| - match = link.match(/\<(?<url>.*)\>\; rel=\"(?<rel>\w+)\"/) + link = response.headers[:link] + return unless link + + link.split(',').map do |link| + match = link.match(/<(?<url>.*)>; rel="(?<rel>\w+)"/) break nil unless match { url: match[:url], rel: match[:rel] } diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb index 6f8c4a59566..b3a2472d702 100644 --- a/qa/qa/support/repeater.rb +++ b/qa/qa/support/repeater.rb @@ -11,7 +11,15 @@ module QA RetriesExceededError = Class.new(RepeaterConditionExceededError) WaitExceededError = Class.new(RepeaterConditionExceededError) - def repeat_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true) + def repeat_until( + max_attempts: nil, + max_duration: nil, + reload_page: nil, + sleep_interval: 0, + raise_on_failure: true, + retry_on_exception: false, + log: true + ) attempts = 0 start = Time.now @@ -29,17 +37,19 @@ module QA raise unless retry_on_exception attempts += 1 - if remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration) - sleep_and_reload_if_needed(sleep_interval, reload_page) + raise unless remaining_attempts?(attempts, max_attempts) && remaining_time?(start, max_duration) - retry - else - raise - end + sleep_and_reload_if_needed(sleep_interval, reload_page) + retry end if raise_on_failure - raise RetriesExceededError, "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" unless remaining_attempts?(attempts, max_attempts) + unless remaining_attempts?(attempts, max_attempts) + raise( + RetriesExceededError, + "Retry condition not met after #{max_attempts} #{'attempt'.pluralize(max_attempts)}" + ) + end raise WaitExceededError, "Wait condition not met after #{max_duration} #{'second'.pluralize(max_duration)}" end diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb index 25dbb42cf6f..fde8ac263ca 100644 --- a/qa/qa/support/retrier.rb +++ b/qa/qa/support/retrier.rb @@ -7,21 +7,21 @@ module QA module_function - def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5) - QA::Runtime::Logger.debug( - <<~MSG.tr("\n", ' ') - with retry_on_exception: max_attempts: #{max_attempts}; - reload_page: #{reload_page}; - sleep_interval: #{sleep_interval} - MSG - ) + def retry_on_exception(max_attempts: 3, reload_page: nil, sleep_interval: 0.5, log: true) + if log + msg = ["with retry_on_exception: max_attempts: #{max_attempts}"] + msg << "reload_page: #{reload_page}" if reload_page + msg << "sleep_interval: #{sleep_interval}" + QA::Runtime::Logger.debug(msg.join('; ')) + end result = nil repeat_until( max_attempts: max_attempts, reload_page: reload_page, sleep_interval: sleep_interval, - retry_on_exception: true + retry_on_exception: true, + log: log ) do result = yield @@ -29,7 +29,7 @@ module QA # We set it to `true` so that it doesn't repeat if there's no exception true end - QA::Runtime::Logger.debug("ended retry_on_exception") + QA::Runtime::Logger.debug("ended retry_on_exception") if log result end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index df3447770be..3e1011dcd2a 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -12,6 +12,7 @@ RSpec.describe QA::Support::Page::Logging do QA::Runtime::Logger.logger = logger allow(Capybara).to receive(:current_session).and_return(page) + allow(page).to receive(:find).and_return(page) allow(page).to receive(:current_url).and_return('http://current-url') allow(page).to receive(:has_css?).with(any_args).and_return(true) end diff --git a/qa/spec/qa_deprecation_toolkit_env.rb b/qa/spec/qa_deprecation_toolkit_env.rb new file mode 100644 index 00000000000..cdd5d954b20 --- /dev/null +++ b/qa/spec/qa_deprecation_toolkit_env.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +require 'deprecation_toolkit' +require 'deprecation_toolkit/rspec' +require 'concurrent/utility/monotonic_time' +require 'active_support/gem_version' + +module QaDeprecationToolkitEnv + # Taken from https://github.com/jeremyevans/ruby-warning/blob/1.1.0/lib/warning.rb#L18 + def self.kwargs_warning + %r{warning: (?:Using the last argument (?:for `.+' )?as keyword parameters is deprecated; maybe \*\* should be added to the call|Passing the keyword argument (?:for `.+' )?as the last hash parameter is deprecated|Splitting the last argument (?:for `.+' )?into positional and keyword parameters is deprecated|The called method (?:`.+' )?is defined here)\n\z} + end + + def self.configure! + # Enable ruby deprecations for keywords, it's suppressed by default in Ruby 2.7.2 + Warning[:deprecated] = true + + DeprecationToolkit::Configuration.test_runner = :rspec + DeprecationToolkit::Configuration.deprecation_path = 'deprecations' + DeprecationToolkit::Configuration.warnings_treated_as_deprecation = [kwargs_warning] + end +end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index c0bedf794be..c6dd56b5f47 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -6,7 +6,7 @@ RSpec.describe QA::Resource::Base do let(:resource) { spy('resource') } let(:location) { 'http://location' } - shared_context 'fabrication context' do + shared_context 'with fabrication context' do subject do Class.new(described_class) do def self.name @@ -28,24 +28,14 @@ RSpec.describe QA::Resource::Base do expect(resource).to receive(:something!).ordered expect(resource).to receive(fabrication_method_used).ordered.and_return(location) - subject.public_send(fabrication_method_called, resource: resource) do |resource| - resource.something! - end - end - - it 'does not log the resource and build method when QA_DEBUG=false' do - stub_env('QA_DEBUG', 'false') - expect(resource).to receive(fabrication_method_used).and_return(location) - - expect { subject.public_send(fabrication_method_called, 'something', resource: resource) } - .not_to output.to_stdout + subject.public_send(fabrication_method_called, resource: resource, &:something!) end end describe '.fabricate!' do context 'when resource does not support fabrication via the API' do before do - expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError) + allow(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError) end it 'calls .fabricate_via_browser_ui!' do @@ -65,7 +55,7 @@ RSpec.describe QA::Resource::Base do end describe '.fabricate_via_api!' do - include_context 'fabrication context' + include_context 'with fabrication context' it_behaves_like 'fabrication method', :fabricate_via_api! @@ -77,18 +67,25 @@ RSpec.describe QA::Resource::Base do expect(result).to eq(resource) end - it 'logs the resource and build method when QA_DEBUG=true' do - stub_env('QA_DEBUG', 'true') - expect(resource).to receive(:fabricate_via_api!).and_return(location) + context "with debug log level" do + before do + allow(QA::Runtime::Logger).to receive(:debug) + end + + it 'logs the resource and build method' do + stub_env('QA_DEBUG', 'true') + + subject.fabricate_via_api!('something', resource: resource, parents: []) - expect { subject.fabricate_via_api!('something', resource: resource, parents: []) } - .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/) - .to_stdout + expect(QA::Runtime::Logger).to have_received(:debug) do |&msg| + expect(msg.call).to match_regex(/==> Built a MyResource via api in [\d.\-e]+ seconds+/) + end + end end end describe '.fabricate_via_browser_ui!' do - include_context 'fabrication context' + include_context 'with fabrication context' it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! @@ -104,16 +101,24 @@ RSpec.describe QA::Resource::Base do expect(result).to eq(resource) end - it 'logs the resource and build method when QA_DEBUG=true' do - stub_env('QA_DEBUG', 'true') + context "with debug log level" do + before do + allow(QA::Runtime::Logger).to receive(:debug) + end + + it 'logs the resource and build method' do + stub_env('QA_DEBUG', 'true') + + subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) } - .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/) - .to_stdout + expect(QA::Runtime::Logger).to have_received(:debug) do |&msg| + expect(msg.call).to match_regex(/==> Built a MyResource via browser_ui in [\d.\-e]+ seconds+/) + end + end end end - shared_context 'simple resource' do + shared_context 'with simple resource' do subject do Class.new(QA::Resource::Base) do attribute :test do @@ -136,7 +141,7 @@ RSpec.describe QA::Resource::Base do end describe '.attribute' do - include_context 'simple resource' + include_context 'with simple resource' context 'when the attribute is populated via a block' do it 'returns value from the block' do @@ -151,7 +156,7 @@ RSpec.describe QA::Resource::Base do let(:api_resource) { { no_block: 'api' } } before do - expect(resource).to receive(:api_resource).and_return(api_resource) + allow(resource).to receive(:api_resource).and_return(api_resource) end it 'returns value from api' do @@ -165,16 +170,16 @@ RSpec.describe QA::Resource::Base do let(:api_resource) { { test: 'api_with_block' } } before do - allow(QA::Runtime::Logger).to receive(:info) + allow(QA::Runtime::Logger).to receive(:debug) end - it 'returns value from api and emits an INFO log entry' do + it 'returns value from api and emits an debug log entry' do result = subject.fabricate!(resource: resource) expect(result).to be_a(described_class) expect(result.test).to eq('api_with_block') expect(QA::Runtime::Logger) - .to have_received(:info).with(/api_with_block/) + .to have_received(:debug).with(/api_with_block/) end end end @@ -209,8 +214,9 @@ RSpec.describe QA::Resource::Base do it 'raises an error because no values could be found' do result = subject.fabricate!(resource: resource) - expect { result.no_block } - .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.") + expect { result.no_block }.to raise_error( + described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}." + ) end end @@ -254,7 +260,7 @@ RSpec.describe QA::Resource::Base do end describe '#web_url' do - include_context 'simple resource' + include_context 'with simple resource' it 'sets #web_url to #current_url after fabrication' do subject.fabricate!(resource: resource) @@ -264,7 +270,7 @@ RSpec.describe QA::Resource::Base do end describe '#visit!' do - include_context 'simple resource' + include_context 'with simple resource' before do allow(resource).to receive(:visit) diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb index 93de2f4a87e..a1de71d31f0 100644 --- a/qa/spec/runtime/api/request_spec.rb +++ b/qa/spec/runtime/api/request_spec.rb @@ -14,7 +14,7 @@ RSpec.describe QA::Runtime::API::Request do end context 'when oauth_access_token is passed in the query string' do - let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) } + let(:request) { described_class.new(client, '/users', oauth_access_token: 'foo') } it 'does not adds a private_token query string' do expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo' diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index f4bfd57504e..0df7b94b894 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -6,6 +6,9 @@ require 'rspec-parameterized' require 'active_support/core_ext/hash' require 'active_support/core_ext/object/blank' +require_relative 'qa_deprecation_toolkit_env' +QaDeprecationToolkitEnv.configure! + if ENV['CI'] && QA::Runtime::Env.knapsack? && !ENV['NO_KNAPSACK'] require 'knapsack' Knapsack::Adapters::RSpecAdapter.bind @@ -23,8 +26,8 @@ Dir[::File.join(__dir__, "support/shared_examples/*.rb")].sort.each { |f| requir RSpec.configure do |config| config.include ::Matchers - QA::Specs::Helpers::Quarantine.configure_rspec - QA::Specs::Helpers::ContextSelector.configure_rspec + config.add_formatter QA::Specs::Helpers::ContextFormatter + config.add_formatter QA::Specs::Helpers::QuarantineFormatter config.before do |example| QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") diff --git a/qa/spec/specs/allure_report_spec.rb b/qa/spec/specs/allure_report_spec.rb new file mode 100644 index 00000000000..27bc0dd3d1d --- /dev/null +++ b/qa/spec/specs/allure_report_spec.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'allure-rspec' + +describe QA::Runtime::AllureReport do + include Helpers::StubENV + + let(:rspec_config) { double('RSpec::Core::Configuration', 'add_formatter': nil, after: nil) } + + let(:png_path) { 'png_path' } + let(:html_path) { 'html_path' } + + let(:allure_config) do + # need to mock config in case the test itself is executed with allure reporting enabled + AllureRspec::RspecConfig.send(:new).tap do |conf| + conf.instance_variable_set(:@allure_config, Allure::Config.send(:new)) + end + end + + before do + stub_env('QA_GENERATE_ALLURE_REPORT', generate_report) + + allow(AllureRspec).to receive(:configure).and_yield(allure_config) + allow(RSpec).to receive(:configure).and_yield(rspec_config) + allow(Capybara::Screenshot).to receive(:after_save_screenshot).and_yield(png_path) + allow(Capybara::Screenshot).to receive(:after_save_html).and_yield(html_path) + end + + context 'with report generation disabled' do + let(:generate_report) { 'false' } + + it 'does not perform configuration' do + aggregate_failures do + expect(described_class.configure!).to be_nil + + expect(AllureRspec).not_to have_received(:configure) + expect(RSpec).not_to have_received(:configure) + expect(Capybara::Screenshot).not_to have_received(:after_save_screenshot) + expect(Capybara::Screenshot).not_to have_received(:after_save_html) + end + end + end + + context 'with report generation enabled' do + let(:generate_report) { 'true' } + + let(:png_file) { 'png-file' } + let(:html_file) { 'html-file' } + let(:ci_job) { 'ee:relative 5' } + + before do + stub_env('CI', 'true') + stub_env('CI_JOB_NAME', ci_job) + + 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 } + + described_class.configure! + end + + it 'configures Allure options' 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).to eq('ee:relative') + end + end + + it 'adds rspec and metadata formatter' do + expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered + expect(rspec_config).to have_received(:add_formatter).with(QA::Support::AllureMetadataFormatter).ordered + end + + it 'configures screenshot saving' do + aggregate_failures do + expect(Allure).to have_received(:add_attachment).with( + name: 'screenshot', + source: png_file, + type: Allure::ContentType::PNG, + test_case: true + ) + expect(Allure).to have_received(:add_attachment).with( + name: 'html', + source: html_file, + type: 'text/html', + test_case: true + ) + end + end + end +end diff --git a/qa/spec/specs/helpers/context_selector_spec.rb b/qa/spec/specs/helpers/context_selector_spec.rb index f0250103008..cbdbe6698ae 100644 --- a/qa/spec/specs/helpers/context_selector_spec.rb +++ b/qa/spec/specs/helpers/context_selector_spec.rb @@ -2,29 +2,25 @@ require 'rspec/core/sandbox' -RSpec.configure do |c| - c.around do |ex| +RSpec.describe QA::Specs::Helpers::ContextSelector do + include Helpers::StubENV + include QA::Specs::Helpers::RSpec + + around do |ex| + QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') + RSpec::Core::Sandbox.sandboxed do |config| + config.formatter = QA::Specs::Helpers::ContextFormatter + # If there is an example-within-an-example, we want to make sure the inner example # does not get a reference to the outer example (the real spec) if it calls # something like `pending` config.before(:context) { RSpec.current_example = nil } - config.color_mode = :off ex.run end end -end - -RSpec.describe QA::Specs::Helpers::ContextSelector do - include Helpers::StubENV - include QA::Specs::Helpers::RSpec - - before do - QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') - described_class.configure_rspec - end describe '.context_matches?' do it 'returns true when url has .com' do @@ -104,7 +100,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do context 'with different environment set' do before do QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.com') - described_class.configure_rspec end it 'does not run against production' do @@ -239,7 +234,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do context 'without CI_PROJECT_NAME set' do before do stub_env('CI_PROJECT_NAME', nil) - described_class.configure_rspec end it 'runs on any pipeline' do @@ -273,7 +267,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do context 'when a pipeline triggered from the default branch runs in gitlab-qa' do before do stub_env('CI_PROJECT_NAME', 'gitlab-qa') - described_class.configure_rspec end it 'runs on default branch pipelines' do @@ -310,7 +303,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do context 'with CI_PROJECT_NAME set' do before do stub_env('CI_PROJECT_NAME', 'NIGHTLY') - described_class.configure_rspec end it 'runs on designated pipeline' do @@ -353,7 +345,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do context 'without CI_JOB_NAME set' do before do stub_env('CI_JOB_NAME', nil) - described_class.configure_rspec end context 'when excluding contexts' do @@ -396,7 +387,6 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do context 'with CI_JOB_NAME set' do before do stub_env('CI_JOB_NAME', 'ee:instance-image') - described_class.configure_rspec end context 'when excluding contexts' do diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index 45754a09b17..548a8510988 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -2,9 +2,14 @@ require 'rspec/core/sandbox' -RSpec.configure do |c| - c.around do |ex| +RSpec.describe QA::Specs::Helpers::Quarantine do + include Helpers::StubENV + include QA::Specs::Helpers::RSpec + + around do |ex| RSpec::Core::Sandbox.sandboxed do |config| + config.formatter = QA::Specs::Helpers::QuarantineFormatter + # If there is an example-within-an-example, we want to make sure the inner example # does not get a reference to the outer example (the real spec) if it calls # something like `pending` @@ -15,18 +20,9 @@ RSpec.configure do |c| ex.run end end -end - -RSpec.describe QA::Specs::Helpers::Quarantine do - include Helpers::StubENV - include QA::Specs::Helpers::RSpec describe '.skip_or_run_quarantined_contexts' do context 'with no tag focused' do - before do - described_class.configure_rspec - end - it 'skips before hooks of quarantined contexts' do executed_hooks = [] @@ -66,7 +62,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do context 'with :quarantine focused' do before do - described_class.configure_rspec RSpec.configure do |c| c.filter_run :quarantine end @@ -110,10 +105,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do describe '.skip_or_run_quarantined_tests_or_contexts' do context 'with no tag focused' do - before do - described_class.configure_rspec - end - it 'skips quarantined tests' do group = describe_successfully do it('is pending', :quarantine) {} @@ -135,7 +126,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do context 'with environment set' do before do QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') - described_class.configure_rspec end context 'no pipeline specified' do @@ -168,7 +158,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do shared_examples 'skipped in project' do |project| before do stub_env('CI_PROJECT_NAME', project) - described_class.configure_rspec end it "is skipped in #{project}" do @@ -209,7 +198,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do context 'with :quarantine focused' do before do - described_class.configure_rspec RSpec.configure do |c| c.filter_run :quarantine end @@ -234,7 +222,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do context 'with a non-quarantine tag focused' do before do - described_class.configure_rspec RSpec.configure do |c| c.filter_run :foo end @@ -277,7 +264,6 @@ RSpec.describe QA::Specs::Helpers::Quarantine do context 'with :quarantine and non-quarantine tags focused' do before do - described_class.configure_rspec RSpec.configure do |c| c.filter_run :foo, :bar, :quarantine end diff --git a/qa/spec/support/allure_metadata_formatter_spec.rb b/qa/spec/support/allure_metadata_formatter_spec.rb new file mode 100644 index 00000000000..cb208642716 --- /dev/null +++ b/qa/spec/support/allure_metadata_formatter_spec.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +describe QA::Support::AllureMetadataFormatter do + include Helpers::StubENV + + let(:formatter) { described_class.new(StringIO.new) } + + let(:rspec_example_notification) { double('RSpec::Core::Notifications::ExampleNotification', example: rspec_example) } + let(:rspec_example) do + double( + 'RSpec::Core::Example', + tms: nil, + issue: nil, + add_link: nil, + attempts: 0, + file_path: 'file/path/spec.rb', + metadata: { + testcase: 'testcase', + quarantine: { issue: 'issue' } + } + ) + end + + let(:ci_job) { 'ee:relative 5' } + let(:ci_job_url) { 'url' } + + before do + stub_env('CI', 'true') + stub_env('CI_JOB_NAME', ci_job) + stub_env('CI_JOB_URL', ci_job_url) + end + + it "adds additional data to report" do + formatter.example_started(rspec_example_notification) + + aggregate_failures do + expect(rspec_example).to have_received(:tms).with('Testcase', 'testcase') + expect(rspec_example).to have_received(:issue).with('Quarantine issue', 'issue') + expect(rspec_example).to have_received(:add_link).with(name: "Job(#{ci_job})", url: ci_job_url) + expect(rspec_example).to have_received(:issue).with( + 'Failure issues', + 'https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=spec.rb' + ) + end + end +end diff --git a/qa/spec/support/matchers/eventually_matcher.rb b/qa/spec/support/matchers/eventually_matcher.rb index 3f0afd6fb54..7a35a3165ae 100644 --- a/qa/spec/support/matchers/eventually_matcher.rb +++ b/qa/spec/support/matchers/eventually_matcher.rb @@ -9,7 +9,7 @@ # expect { Something.that.takes.time.to_appear }.not_to eventually_eq(expected_result) # # With duration and attempts override -# expect { Something.that.takes.time.to_appear }.to eventually_eq(expected_result).within(duration: 10, attempts: 5) +# expect { Something.that.takes.time.to_appear }.to eventually_eq(expected_result).within(max_duration: 10, max_attempts: 5) module Matchers %w[ @@ -21,9 +21,9 @@ module Matchers be_empty ].each do |op| RSpec::Matchers.define(:"eventually_#{op}") do |*expected| - chain(:within) do |options = {}| - @duration = options[:duration] - @attempts = options[:attempts] + chain(:within) do |kwargs = {}| + @retry_args = kwargs + @retry_args[:sleep_interval] = 0.5 unless @retry_args[:sleep_interval] end def supports_block_expectations? @@ -52,11 +52,12 @@ module Matchers # @param [Symbol] expectation_name # @return [Boolean] def wait_and_check(actual, expectation_name) - QA::Support::Retrier.retry_until( - max_attempts: @attempts, - max_duration: @duration, - sleep_interval: 0.5 - ) do + attempt = 0 + + QA::Runtime::Logger.debug("Running eventually matcher with '#{operator_msg}' operator") + QA::Support::Retrier.retry_until(**@retry_args) do + QA::Runtime::Logger.debug("evaluating expectation, attempt: #{attempt += 1}") + public_send(expectation_name, actual) rescue RSpec::Expectations::ExpectationNotMetError, QA::Resource::ApiFabricator::ResourceNotFoundError false diff --git a/qa/spec/support/retrier_spec.rb b/qa/spec/support/retrier_spec.rb index 6f052519516..4e27915553c 100644 --- a/qa/spec/support/retrier_spec.rb +++ b/qa/spec/support/retrier_spec.rb @@ -70,8 +70,9 @@ RSpec.describe QA::Support::Retrier do describe '.retry_on_exception' do context 'when the condition is true' do it 'logs max_attempts, reload_page, and sleep_interval parameters' do - expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { true } } - .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process + message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/ + expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { true } } + .to output(message).to_stdout_from_any_process end it 'logs the end' do @@ -82,8 +83,9 @@ RSpec.describe QA::Support::Retrier do context 'when the condition is false' do it 'logs the start' do - expect { subject.retry_on_exception(max_attempts: 1, reload_page: nil, sleep_interval: 0) { false } } - .to output(/with retry_on_exception: max_attempts: 1; reload_page: ; sleep_interval: 0/).to_stdout_from_any_process + message = /with retry_on_exception: max_attempts: 1; reload_page: true; sleep_interval: 0/ + expect { subject.retry_on_exception(max_attempts: 1, reload_page: true, sleep_interval: 0) { false } } + .to output(message).to_stdout_from_any_process end it 'logs the end' do |