diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-20 02:18:09 +0300 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-09-20 02:18:09 +0300 |
commit | 6ed4ec3e0b1340f96b7c043ef51d1b33bbe85fde (patch) | |
tree | dc4d20fe6064752c0bd323187252c77e0a89144b /qa | |
parent | 9868dae7fc0655bd7ce4a6887d4e6d487690eeed (diff) |
Add latest changes from gitlab-org/gitlab@15-4-stable-eev15.4.0-rc42
Diffstat (limited to 'qa')
171 files changed, 2260 insertions, 873 deletions
diff --git a/qa/.confiner/master.yml b/qa/.confiner/master.yml index bfb44facd7d..e6fc3e68747 100644 --- a/qa/.confiner/master.yml +++ b/qa/.confiner/master.yml @@ -4,7 +4,7 @@ args: threshold: 3 # 3 failures private_token: $QA_GITLAB_CI_TOKEN - project_id: gitlab-org/gitlab-qa-mirror # https://gitlab.com/gitlab-org/gitlab-qa-mirror/ + project_id: gitlab-org/gitlab target_project: gitlab-org/gitlab failure_issue_labels: QA,Quality failure_issue_prefix: "Failure in " diff --git a/qa/Gemfile b/qa/Gemfile index 7c46d35bb48..cf939f8e301 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -2,7 +2,7 @@ source 'https://rubygems.org' -gem 'gitlab-qa', '~> 7', require: 'gitlab/qa' +gem 'gitlab-qa', '~> 8', require: 'gitlab/qa' gem 'activesupport', '~> 6.1.4.7' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.16.0' gem 'capybara', '~> 3.35.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index ff382788c5a..dd14b675769 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -11,8 +11,8 @@ GEM adamantium (0.2.0) ice_nine (~> 0.11.0) memoizable (~> 0.4.0) - addressable (2.8.0) - public_suffix (>= 2.0.2, < 5.0) + addressable (2.8.1) + public_suffix (>= 2.0.2, < 6.0) airborne (0.3.4) activesupport rack @@ -118,13 +118,14 @@ GEM gitlab (4.18.0) httparty (~> 0.18) terminal-table (>= 1.5.1) - gitlab-qa (7.33.0) + gitlab-qa (8.4.2) activesupport (~> 6.1) gitlab (~> 4.18.0) http (~> 5.0) nokogiri (~> 1.10) rainbow (~> 3.0.0) table_print (= 1.5.7) + zeitwerk (~> 2.4) google-apis-compute_v1 (0.21.0) google-apis-core (>= 0.4, < 2.a) google-apis-core (0.4.1) @@ -157,7 +158,7 @@ GEM multi_json (~> 1.11) os (>= 0.9, < 2.0) signet (>= 0.16, < 2.a) - http (5.0.4) + http (5.1.0) addressable (~> 2.8) http-cookie (~> 1.0) http-form_data (~> 2.2) @@ -170,7 +171,7 @@ GEM mime-types (~> 3.0) multi_xml (>= 0.5.2) httpclient (2.8.3) - i18n (1.10.0) + i18n (1.12.0) concurrent-ruby (~> 1.0) ice_nine (0.11.2) influxdb-client (1.17.0) @@ -193,7 +194,7 @@ GEM mime-types-data (3.2022.0105) mini_mime (1.1.0) mini_portile2 (2.8.0) - minitest (5.15.0) + minitest (5.16.3) multi_json (1.15.0) multi_xml (0.6.0) multipart-post (2.1.1) @@ -209,7 +210,7 @@ GEM parallel (1.19.2) parallel_tests (2.29.0) parallel - parser (3.0.3.2) + parser (3.1.2.1) ast (~> 2.4.1) proc_to_ast (0.1.0) coderay @@ -222,7 +223,7 @@ GEM pry-byebug (3.5.1) byebug (~> 9.1) pry (~> 0.10) - public_suffix (4.0.7) + public_suffix (5.0.0) racc (1.6.0) rack (2.2.3.1) rack-test (1.1.0) @@ -290,13 +291,13 @@ GEM thread_safe (0.3.6) timecop (0.9.1) trailblazer-option (0.1.2) - tzinfo (2.0.4) + tzinfo (2.0.5) concurrent-ruby (~> 1.0) uber (0.1.0) unf (0.1.4) unf_ext unf_ext (0.0.8.2) - unicode-display_width (2.1.0) + unicode-display_width (2.2.0) unparser (0.4.7) abstract_type (~> 0.0.7) adamantium (~> 0.2.0) @@ -335,7 +336,7 @@ DEPENDENCIES deprecation_toolkit (~> 1.5.1) faker (~> 2.19, >= 2.19.0) fog-google (~> 1.17) - gitlab-qa (~> 7) + gitlab-qa (~> 8) influxdb-client (~> 1.17) knapsack (~> 4.0) nokogiri (~> 1.12) diff --git a/qa/README.md b/qa/README.md index dbc70f55f1c..564beb4c6e8 100644 --- a/qa/README.md +++ b/qa/README.md @@ -1,12 +1,12 @@ # GitLab QA - End-to-end tests for GitLab -This directory contains [end-to-end tests](../../../doc/development/testing_guide/end_to_end/index.md) +This directory contains [end-to-end tests](../doc/development/testing_guide/end_to_end/index.md) for GitLab. It includes the test framework and the tests themselves. The tests can be found in `qa/specs/features` (not to be confused with the unit tests for the test framework, which are in `spec/`). -It is part of the [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa). +Tests use [GitLab QA project](https://gitlab.com/gitlab-org/gitlab-qa) for environment orchestration in CI jobs. ## What is it? @@ -46,7 +46,7 @@ Note that tests are using `Chrome` web browser by default so it should be instal Tests are executed in merge request pipelines as part of the development lifecycle. - [Review app environment](../doc/development/testing_guide/review_apps.md) -- [package-and-qa](../doc/development/testing_guide/end_to_end/index.md#testing-code-in-merge-requests) +- [e2e:package-and-test](../doc/development/testing_guide/end_to_end/index.md#testing-code-in-merge-requests) ### Logging diff --git a/qa/Rakefile b/qa/Rakefile index d3e39d8ed1e..ada27596ae4 100644 --- a/qa/Rakefile +++ b/qa/Rakefile @@ -4,23 +4,18 @@ require_relative "qa" Dir['tasks/*.rake'].each { |file| load file } -desc "Revokes all personal access tokens" -task :revoke_personal_access_tokens do - QA::Tools::RevokeAllPersonalAccessTokens.new.run -end - desc "Deletes subgroups within a provided group" task :delete_subgroups do QA::Tools::DeleteSubgroups.new.run end desc "Initialize GitLab with an access token" -task :initialize_gitlab_auth, [:address] do |t, args| +task :initialize_gitlab_auth, [:address] do |_, args| QA::Tools::InitializeGitLabAuth.new(args).run end desc "Generate Performance Testdata" -task :generate_perf_testdata, :type do |t, args| +task :generate_perf_testdata, :type do |_, args| args.with_defaults(type: :all) QA::Tools::GeneratePerfTestdata.new.method(args[:type]).call end @@ -50,7 +45,7 @@ desc "Generate data and run load tests" task generate_data_and_run_load_test: [:generate_perf_testdata, :run_artillery_load_tests] desc "Deletes test ssh keys a user" -task :delete_test_ssh_keys, [:title_portion, :delete_before, :dry_run] do |t, args| +task :delete_test_ssh_keys, [:title_portion, :delete_before, :dry_run] do |_, args| QA::Tools::DeleteTestSSHKeys.new(args).run end @@ -60,33 +55,38 @@ task :delete_projects do end desc "Deletes test users" -task :delete_test_users, [:delete_before, :dry_run, :exclude_users] do |t, args| +task :delete_test_users, [:delete_before, :dry_run, :exclude_users] do |_, args| QA::Tools::DeleteTestUsers.new(args).run end desc "Deletes snippets" -task :delete_test_snippets, [:delete_before, :dry_run] do |t, args| +task :delete_test_snippets, [:delete_before, :dry_run] do |_, args| QA::Tools::DeleteTestSnippets.new(args).run end namespace :test_resources do desc "Deletes resources created during E2E test runs" - task :delete, [:file_pattern] do |t, args| + task :delete, [:file_pattern] do |_, args| QA::Tools::TestResourcesHandler.new(args[:file_pattern]).run_delete end desc "Upload test resources JSON files to GCS" - task :upload, [:file_pattern, :ci_project_name] do |t, args| + task :upload, [:file_pattern, :ci_project_name] do |_, args| QA::Tools::TestResourcesHandler.new(args[:file_pattern]).upload(args[:ci_project_name]) end desc "Download test resources JSON files from GCS" - task :download, [:ci_project_name] do |t, args| + task :download, [:ci_project_name] do |_, args| QA::Tools::TestResourcesHandler.new.download(args[:ci_project_name]) end end desc "Deletes user's projects" -task :delete_user_projects, [:delete_before, :dry_run] do |t, args| +task :delete_user_projects, [:delete_before, :dry_run] do |_, args| QA::Tools::DeleteUserProjects.new(args).run end + +desc "Revokes user's personal access tokens" +task :revoke_user_pats, [:revoke_before, :dry_run] do |_, args| + QA::Tools::RevokeUserPersonalAccessTokens.new(args).run +end diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb index 1538384f6ed..ef73bad2879 100644 --- a/qa/lib/gitlab/page/admin/subscription.rb +++ b/qa/lib/gitlab/page/admin/subscription.rb @@ -10,8 +10,8 @@ module Gitlab text_field :activation_code button :activate label :terms_of_services, text: /I agree that/ - link :remove_license, 'data-testid': 'license-remove-action' - button :confirm_ok_button + button :remove_license + button :confirm_remove_license p :plan p :started p :name @@ -30,6 +30,11 @@ module Gitlab terms_of_services_element.click # workaround for hidden checkbox end + def remove_license_file + remove_license + confirm_remove_license + end + # Checks if a subscription record exists in subscription history table # # @param plan [Hash] Name of the plan @@ -51,7 +51,6 @@ module QA "repo_by_url" => "RepoByURL", "oauth" => "OAuth", "saml_sso_sign_in" => "SamlSSOSignIn", - "saml_sso_sign_up" => "SamlSSOSignUp", "group_saml" => "GroupSAML", "instance_saml" => "InstanceSAML", "saml_sso" => "SamlSSO", diff --git a/qa/qa/fixtures/auto_devops_rack/Dockerfile b/qa/qa/fixtures/auto_devops_rack/Dockerfile deleted file mode 100644 index 6ab2795dd40..00000000000 --- a/qa/qa/fixtures/auto_devops_rack/Dockerfile +++ /dev/null @@ -1,9 +0,0 @@ -FROM ruby:2.6.5-alpine -ADD ./ /app/ -WORKDIR /app -ENV RACK_ENV production -ENV PORT 5000 -EXPOSE 5000 - -RUN bundle install -CMD ["bundle","exec", "rackup", "-p", "5000"] diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile b/qa/qa/fixtures/auto_devops_rack/Gemfile deleted file mode 100644 index 2c7c77adf94..00000000000 --- a/qa/qa/fixtures/auto_devops_rack/Gemfile +++ /dev/null @@ -1,5 +0,0 @@ -# frozen_string_literal: true - -source 'https://rubygems.org' -gem 'rack' -gem 'rake' diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock deleted file mode 100644 index 04a85be4b2f..00000000000 --- a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock +++ /dev/null @@ -1,15 +0,0 @@ -GEM - remote: https://rubygems.org/ - specs: - rack (2.2.3) - rake (12.3.3) - -PLATFORMS - ruby - -DEPENDENCIES - rack - rake - -BUNDLED WITH - 1.17.3 diff --git a/qa/qa/fixtures/auto_devops_rack/Rakefile b/qa/qa/fixtures/auto_devops_rack/Rakefile deleted file mode 100644 index a6d08103d55..00000000000 --- a/qa/qa/fixtures/auto_devops_rack/Rakefile +++ /dev/null @@ -1,9 +0,0 @@ -# frozen_string_literal: true - -require 'rake/testtask' - -task default: %w[test] - -task :test do - puts "ok" -end diff --git a/qa/qa/fixtures/auto_devops_rack/config.ru b/qa/qa/fixtures/auto_devops_rack/config.ru deleted file mode 100644 index aea28ef1893..00000000000 --- a/qa/qa/fixtures/auto_devops_rack/config.ru +++ /dev/null @@ -1,3 +0,0 @@ -# frozen_string_literal: true - -run lambda { |env| [200, { 'Content-Type' => 'text/plain' }, StringIO.new("Hello World! #{ENV['OPTIONAL_MESSAGE']}\n")] } diff --git a/qa/qa/fixtures/package_managers/terraform/module_upload.yaml.erb b/qa/qa/fixtures/package_managers/terraform/module_upload.yaml.erb new file mode 100644 index 00000000000..3a7313b0712 --- /dev/null +++ b/qa/qa/fixtures/package_managers/terraform/module_upload.yaml.erb @@ -0,0 +1,21 @@ +stages: + - upload + +upload: + stage: upload + image: curlimages/curl:latest + variables: + TERRAFORM_MODULE_DIR: ${CI_PROJECT_DIR} # The path to your Terraform module + TERRAFORM_MODULE_NAME: ${CI_PROJECT_NAME} # The name of your Terraform module + TERRAFORM_MODULE_SYSTEM: local # The system or provider your Terraform module targets (ex. local, aws, google) + TERRAFORM_MODULE_VERSION: ${CI_COMMIT_TAG} # Tag commits with SemVer for the version of your Terraform module to be published + script: + - TERRAFORM_MODULE_NAME=$(echo "${TERRAFORM_MODULE_NAME}" | tr " _" -) # module-name must not have spaces or underscores, so translate them to hyphens + - tar -vczf ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz -C ${TERRAFORM_MODULE_DIR} --exclude=./.git . + - 'curl --location --header "JOB-TOKEN: ${CI_JOB_TOKEN}" + --upload-file ${TERRAFORM_MODULE_NAME}-${TERRAFORM_MODULE_SYSTEM}-${TERRAFORM_MODULE_VERSION}.tgz + ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/terraform/modules/${TERRAFORM_MODULE_NAME}/${TERRAFORM_MODULE_SYSTEM}/${TERRAFORM_MODULE_VERSION}/file' + rules: + - if: $CI_COMMIT_TAG + tags: + - runner-for-<%= imported_project.name %>
\ No newline at end of file diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb index 05f114acbc5..ec205e0aa86 100644 --- a/qa/qa/flow/login.rb +++ b/qa/qa/flow/login.rb @@ -3,7 +3,7 @@ module QA module Flow module Login - module_function + extend self def while_signed_in(as: nil, address: :gitlab, admin: false) sign_in(as: as, address: address, admin: admin) @@ -52,3 +52,5 @@ module QA end end end + +QA::Flow::Login.prepend_mod_with('Flow::Login', namespace: QA) diff --git a/qa/qa/flow/merge_request.rb b/qa/qa/flow/merge_request.rb index f1cab2c7d1a..24abfa9e356 100644 --- a/qa/qa/flow/merge_request.rb +++ b/qa/qa/flow/merge_request.rb @@ -3,11 +3,10 @@ module QA module Flow module MergeRequest - module_function + extend self def enable_merge_trains - Page::Project::Menu.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform(&:expand_merge_requests_settings) + Page::Project::Menu.perform(&:go_to_merge_request_settings) Page::Project::Settings::MergeRequest.perform(&:enable_merge_train) end @@ -33,3 +32,5 @@ module QA end end end + +QA::Flow::MergeRequest.prepend_mod_with('Flow::MergeRequest', namespace: QA) diff --git a/qa/qa/flow/pipeline.rb b/qa/qa/flow/pipeline.rb index d19b2530bb8..fb6a5425a6e 100644 --- a/qa/qa/flow/pipeline.rb +++ b/qa/qa/flow/pipeline.rb @@ -3,7 +3,7 @@ module QA module Flow module Pipeline - module_function + extend self # Acceptable statuses: # canceled, created, failed, manual, passed @@ -27,3 +27,5 @@ module QA end end end + +QA::Flow::Pipeline.prepend_mod_with('Flow::Pipeline', namespace: QA) diff --git a/qa/qa/flow/project.rb b/qa/qa/flow/project.rb index 397806b33a3..70bdcfcb719 100644 --- a/qa/qa/flow/project.rb +++ b/qa/qa/flow/project.rb @@ -3,7 +3,7 @@ module QA module Flow module Project - module_function + extend self def go_to_create_project_from_template Page::Project::New.perform(&:click_create_from_template_link) @@ -11,3 +11,5 @@ module QA end end end + +QA::Flow::Project.prepend_mod_with('Flow::Project', namespace: QA) diff --git a/qa/qa/flow/purchase.rb b/qa/qa/flow/purchase.rb index e0efa8a8178..c07e03c104d 100644 --- a/qa/qa/flow/purchase.rb +++ b/qa/qa/flow/purchase.rb @@ -5,7 +5,7 @@ module QA module Purchase include QA::Support::Helpers::Plan - module_function + extend self def upgrade_subscription(plan: PREMIUM) Page::Group::Menu.perform(&:go_to_billing) diff --git a/qa/qa/flow/saml.rb b/qa/qa/flow/saml.rb index 1280f59c3c2..8a0ebb9f551 100644 --- a/qa/qa/flow/saml.rb +++ b/qa/qa/flow/saml.rb @@ -3,7 +3,7 @@ module QA module Flow module Saml - module_function + extend self def page Capybara.current_session @@ -72,3 +72,5 @@ module QA end end end + +QA::Flow::Saml.prepend_mod_with('Flow::Saml', namespace: QA) diff --git a/qa/qa/flow/settings.rb b/qa/qa/flow/settings.rb index 775b7686c10..8e6ce667475 100644 --- a/qa/qa/flow/settings.rb +++ b/qa/qa/flow/settings.rb @@ -3,7 +3,7 @@ module QA module Flow module Settings - module_function + extend self def disable_snowplow Flow::Login.while_signed_in_as_admin do @@ -23,3 +23,5 @@ module QA end end end + +QA::Flow::Settings.prepend_mod_with('Flow::Settings', namespace: QA) diff --git a/qa/qa/flow/sign_up.rb b/qa/qa/flow/sign_up.rb index ec7886ef969..52c92293bad 100644 --- a/qa/qa/flow/sign_up.rb +++ b/qa/qa/flow/sign_up.rb @@ -3,7 +3,7 @@ module QA module Flow module SignUp - module_function + extend self def page Capybara.current_session @@ -55,3 +55,5 @@ module QA end end end + +QA::Flow::SignUp.prepend_mod_with('Flow::SignUp', namespace: QA) diff --git a/qa/qa/flow/user.rb b/qa/qa/flow/user.rb index c0bd475adb7..8f11d4ef26f 100644 --- a/qa/qa/flow/user.rb +++ b/qa/qa/flow/user.rb @@ -3,7 +3,7 @@ module QA module Flow module User - module_function + extend self def page Capybara.current_session @@ -24,3 +24,5 @@ module QA end end end + +QA::Flow::User.prepend_mod_with('Flow::User', namespace: QA) diff --git a/qa/qa/flow/user_onboarding.rb b/qa/qa/flow/user_onboarding.rb index 066e1878869..62397d5641d 100644 --- a/qa/qa/flow/user_onboarding.rb +++ b/qa/qa/flow/user_onboarding.rb @@ -3,7 +3,7 @@ module QA module Flow module UserOnboarding - module_function + extend self def onboard_user Page::Registration::Welcome.perform do |welcome_page| @@ -17,3 +17,5 @@ module QA end end end + +QA::Flow::UserOnboarding.prepend_mod_with('Flow::UserOnboarding', namespace: QA) diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index d7e0101ff2c..03f753b1d61 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -500,3 +500,5 @@ module QA end end end + +QA::Page::Base.prepend_mod_with('Page::Base', namespace: QA) diff --git a/qa/qa/page/component/access_tokens.rb b/qa/qa/page/component/access_tokens.rb index 36c0f8c2f00..3c8a608cdc2 100644 --- a/qa/qa/page/component/access_tokens.rb +++ b/qa/qa/page/component/access_tokens.rb @@ -18,6 +18,10 @@ module QA element :expiry_date_field end + base.view 'app/views/shared/access_tokens/_created_container.html.haml' do + element :created_access_token_field + end + base.view 'app/views/shared/access_tokens/_form.html.haml' do element :access_token_name_field element :create_token_button @@ -32,7 +36,7 @@ module QA end base.view 'app/assets/javascripts/access_tokens/components/new_access_token_app.vue' do - element :created_access_token + element :created_access_token_field end base.view 'app/assets/javascripts/access_tokens/components/access_token_table_app.vue' do @@ -53,7 +57,7 @@ module QA end def created_access_token - find_element(:created_access_token, wait: 30).value + find_element(:created_access_token_field, wait: 30).value end def fill_expiry_date(date) diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb index 2ba198621fc..485e363d960 100644 --- a/qa/qa/page/component/ci_badge_link.rb +++ b/qa/qa/page/component/ci_badge_link.rb @@ -32,12 +32,12 @@ module QA super base.view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do - element :status_badge + element :status_badge_link end end def status_badge - find_element(:status_badge).text + find_element(:status_badge_link).text end def completed?(timeout: 60) diff --git a/qa/qa/page/component/content_editor.rb b/qa/qa/page/component/content_editor.rb index b3a42634fe7..f7b055b6052 100644 --- a/qa/qa/page/component/content_editor.rb +++ b/qa/qa/page/component/content_editor.rb @@ -21,6 +21,10 @@ module QA base.view 'app/assets/javascripts/content_editor/components/toolbar_image_button.vue' do element :file_upload_field end + + base.view 'app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue' do + element :wiki_hidden_content + end end def add_heading(heading, text) @@ -41,6 +45,11 @@ module QA text_area.send_keys(:return) find_element(:file_upload_field, visible: false).send_keys(image_path) end + + QA::Support::Retrier.retry_on_exception do + source = find_element(:wiki_hidden_content, visible: false) + source.value =~ %r{uploads/.*#{::File.basename(image_path)}} + end end private diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb index 8494518723d..37c833e77c2 100644 --- a/qa/qa/page/component/note.rb +++ b/qa/qa/page/component/note.rb @@ -28,7 +28,7 @@ module QA end base.view 'app/assets/javascripts/notes/components/discussion_filter.vue' do - element :discussion_filter_dropdown, required: true + element :discussion_preferences_dropdown, required: true element :filter_menu_item end @@ -169,7 +169,7 @@ module QA def select_filter_with_text(text) retry_on_exception do click_element(:title_content) - click_element :discussion_filter_dropdown + click_element :discussion_preferences_dropdown find_element(:filter_menu_item, text: text).click wait_for_requests diff --git a/qa/qa/page/component/project/templates.rb b/qa/qa/page/component/project/templates.rb index 8baf15acdff..6e86455fc52 100644 --- a/qa/qa/page/component/project/templates.rb +++ b/qa/qa/page/component/project/templates.rb @@ -5,7 +5,7 @@ module QA module Project module Templates def use_template_for_project(project_name) - within find_element(:template_option_row, text: project_name) do + within find_element(:template_option_container, text: project_name) do click_element :use_template_button end end diff --git a/qa/qa/page/component/web_ide/modal/create_new_file.rb b/qa/qa/page/component/web_ide/modal/create_new_file.rb index 7c55f775476..2869bb9c331 100644 --- a/qa/qa/page/component/web_ide/modal/create_new_file.rb +++ b/qa/qa/page/component/web_ide/modal/create_new_file.rb @@ -9,7 +9,7 @@ module QA view 'app/assets/javascripts/ide/components/new_dropdown/modal.vue' do element :file_name_field, required: true element :new_file_modal, required: true - element :template_list + element :template_list_content end end end diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb index 22b9a4c8b0d..9e558844469 100644 --- a/qa/qa/page/component/wiki_page_form.rb +++ b/qa/qa/page/component/wiki_page_form.rb @@ -35,6 +35,10 @@ module QA end def click_submit + # In case any changes were just made, wait for the hidden content field to be updated via a deferred call + # before clicking submit. See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/97693#note_1098728562 + sleep 0.5 + click_element(:wiki_submit_button) QA::Support::Retrier.retry_on_exception do diff --git a/qa/qa/page/file/shared/commit_message.rb b/qa/qa/page/file/shared/commit_message.rb index a3658fa11af..12154cdb728 100644 --- a/qa/qa/page/file/shared/commit_message.rb +++ b/qa/qa/page/file/shared/commit_message.rb @@ -14,6 +14,10 @@ module QA element :commit_message_field end + base.view 'app/assets/javascripts/repository/components/last_commit.vue' do + element :commit_content + end + base.view 'app/views/shared/_commit_message_container.html.haml' do element :commit_message_field end diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb index 581aa835ada..31899d9a0c7 100644 --- a/qa/qa/page/file/show.rb +++ b/qa/qa/page/file/show.rb @@ -14,11 +14,6 @@ module QA element :lock_button end - view 'app/helpers/blob_helper.rb' do - element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern - element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern - end - view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do element :edit_button end diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb index c80bdadb11f..58febaddfa0 100644 --- a/qa/qa/page/group/members.rb +++ b/qa/qa/page/group/members.rb @@ -9,7 +9,7 @@ module QA include Page::Component::MembersFilter view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do - element :remove_member_modal_content + element :remove_member_modal end view 'app/assets/javascripts/pages/groups/group_members/index.js' do @@ -45,7 +45,7 @@ module QA click_element :delete_member_button end - within_element(:remove_member_modal_content) do + within_element(:remove_member_modal) do click_button("Remove member") end end diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb index 783fbd25929..de065ca187d 100644 --- a/qa/qa/page/group/menu.rb +++ b/qa/qa/page/group/menu.rb @@ -47,7 +47,7 @@ module QA def go_to_package_settings hover_group_settings do within_submenu do - click_element(:sidebar_menu_item_link, menu_item: 'Packages & Registries') + click_element(:sidebar_menu_item_link, menu_item: 'Packages and registries') end end end @@ -122,8 +122,8 @@ module QA 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 + scroll_to_element(:sidebar_menu_link, menu_item: 'Packages and registries') + find_element(:sidebar_menu_link, menu_item: 'Packages and registries').hover yield end diff --git a/qa/qa/page/group/settings/package_registries.rb b/qa/qa/page/group/settings/package_registries.rb index 35186159dc9..bcf51f0f223 100644 --- a/qa/qa/page/group/settings/package_registries.rb +++ b/qa/qa/page/group/settings/package_registries.rb @@ -8,8 +8,7 @@ module QA view 'app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue' do element :package_registry_settings_content - element :reject_duplicates_toggle - element :reject_duplicates_label + element :allow_duplicates_toggle end view 'app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue' do @@ -17,32 +16,32 @@ module QA element :dependency_proxy_setting_toggle end - def set_reject_duplicates_disabled + def set_allow_duplicates_disabled within_element :package_registry_settings_content do - click_on_reject_duplicates_button if duplicates_disabled? + click_on_allow_duplicates_button if duplicates_enabled? end end - def set_reject_duplicates_enabled + def set_allow_duplicates_enabled within_element :package_registry_settings_content do - click_on_reject_duplicates_button unless duplicates_disabled? + click_on_allow_duplicates_button unless duplicates_enabled? end end - def click_on_reject_duplicates_button - with_reject_duplicates_button do |button| + def click_on_allow_duplicates_button + with_allow_duplicates_button do |button| button.click end end - def duplicates_disabled? - with_reject_duplicates_button do |button| + def duplicates_enabled? + with_allow_duplicates_button do |button| button[:class].include?('is-checked') end end - def with_reject_duplicates_button - within_element :reject_duplicates_toggle do + def with_allow_duplicates_button + within_element :allow_duplicates_toggle do toggle = find('button.gl-toggle:not(.is-disabled)') yield(toggle) end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 90b419f8cef..aaf10e12e82 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -53,28 +53,45 @@ module QA element :search_term_field end + view 'app/views/layouts/header/_new_dropdown.html.haml' do + element :new_menu_toggle + end + + view 'app/helpers/nav/new_dropdown_helper.rb' do + element :global_new_group_link + element :global_new_project_link + end + def go_to_groups within_groups_menu do - click_element(:menu_item_link, title: 'Your groups') + # Remove if statement once :remove_extra_primary_submenu_options ff is enabled by default + if has_element?(:menu_item_link, title: 'Your groups') + click_element(:menu_item_link, title: 'Your groups') + else + click_element(:menu_item_link, title: 'View all groups') + end end end def go_to_create_group - within_groups_menu do - click_element(:menu_item_link, title: 'Create group') - end + click_element(:new_menu_toggle) + click_element(:global_new_group_link) end def go_to_projects within_projects_menu do - click_element(:menu_item_link, title: 'Your projects') + # Remove if statement once :remove_extra_primary_submenu_options ff is enabled by default + if has_element?(:menu_item_link, title: 'Your projects') + click_element(:menu_item_link, title: 'Your projects') + else + click_element(:menu_item_link, title: 'View all projects') + end end end def go_to_create_project - within_projects_menu do - click_element(:menu_item_link, title: 'Create new project') - end + click_element(:new_menu_toggle) + click_element(:global_new_project_link) end def go_to_menu_dropdown_option(option_name) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 9fc0cf0ccf8..2587241ed18 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -363,7 +363,7 @@ module QA # Revisit after merge page re-architect is done https://gitlab.com/gitlab-org/gitlab/-/issues/300042 # To remove page refresh logic if possible wait_until_ready_to_merge - wait_until { !find_element(:merge_button).has_text?("when pipeline succeeds") } + wait_until { !find_element(:merge_button).text.include?('when pipeline succeeds') } click_element(:merge_button) end diff --git a/qa/qa/page/profile/ssh_keys.rb b/qa/qa/page/profile/ssh_keys.rb index db71062cec6..0653df62560 100644 --- a/qa/qa/page/profile/ssh_keys.rb +++ b/qa/qa/page/profile/ssh_keys.rb @@ -5,12 +5,15 @@ module QA module Profile class SSHKeys < Page::Base view 'app/views/profiles/keys/_form.html.haml' do - element :key_expiry_date_field element :key_title_field element :key_public_key_field element :add_key_button end + view 'app/assets/javascripts/access_tokens/components/expires_at_field.vue' do + element :expiry_date_field + end + view 'app/helpers/ssh_keys_helper.rb' do element :delete_ssh_key_button element :ssh_key_delete_modal @@ -25,19 +28,21 @@ module QA fill_element(:key_title_field, title) # Expire in 2 days just in case the key is created just before midnight fill_expiry_date(Date.today + 2) + # Close the datepicker + find_element(:expiry_date_field).find('input').send_keys(:enter) click_element(:add_key_button) end def fill_expiry_date(date) - date = date.strftime('%m/%d/%Y') if date.is_a?(Date) + date = date.strftime('%Y-%m-%d') if date.is_a?(Date) begin - Date.strptime(date, '%m/%d/%Y') + Date.strptime(date, '%Y-%m-%d') rescue ArgumentError - raise "Expiry date must be in mm/dd/yyyy format" + raise "Expiry date must be in YYYY-MM-DD format" end - fill_element(:key_expiry_date_field, date) + fill_element(:expiry_date_field, date) end def remove_key(title) diff --git a/qa/qa/page/project/fork/new.rb b/qa/qa/page/project/fork/new.rb index e1b5e47dd0b..b622b341685 100644 --- a/qa/qa/page/project/fork/new.rb +++ b/qa/qa/page/project/fork/new.rb @@ -6,19 +6,40 @@ module QA module Fork class New < Page::Base view 'app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue' do - element :fork_namespace_dropdown element :fork_project_button element :fork_privacy_button end + view 'app/assets/javascripts/pages/projects/forks/new/components/project_namespace.vue' do + element :select_namespace_dropdown + element :select_namespace_dropdown_item + element :select_namespace_dropdown_search_field + element :select_namespace_dropdown_item + end + def fork_project(namespace = Runtime::Namespace.path) - select_element(:fork_namespace_dropdown, namespace) + choose_namespace(namespace) click_element(:fork_privacy_button, privacy_level: 'public') click_element(:fork_project_button) end - def fork_namespace_dropdown_values - find_element(:fork_namespace_dropdown).all(:option).map { |option| option.text.tr("\n", '').strip } + def get_list_of_namespaces + click_element(:select_namespace_dropdown) + wait_until(reload: false) do + has_element?(:select_namespace_dropdown_item) + end + all_elements(:select_namespace_dropdown_item, minimum: 1).map(&:text) + end + + def choose_namespace(namespace) + retry_on_exception do + click_element(:select_namespace_dropdown) + fill_element(:select_namespace_dropdown_search_field, namespace) + wait_until(reload: false) do + has_element?(:select_namespace_dropdown_item, text: namespace) + end + click_button(namespace) + end end end end diff --git a/qa/qa/page/project/infrastructure/kubernetes/index.rb b/qa/qa/page/project/infrastructure/kubernetes/index.rb index 34d2ad55429..4c759a049e1 100644 --- a/qa/qa/page/project/infrastructure/kubernetes/index.rb +++ b/qa/qa/page/project/infrastructure/kubernetes/index.rb @@ -10,18 +10,13 @@ module QA element :clusters_actions_button end - def connect_existing_cluster - within_element(:clusters_actions_button) { click_button(class: 'dropdown-toggle-split') } - click_link 'Connect a cluster (certificate - deprecated)' + def connect_cluster + click_element(:clusters_actions_button) end def has_cluster?(cluster) has_element?(:cluster, cluster_name: cluster.to_s) end - - def click_on_cluster(cluster) - click_on cluster.cluster_name - end end end end diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 2fb925b3930..5506c5ed4d9 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -11,15 +11,15 @@ module QA element :job_log_content end - view 'app/assets/javascripts/jobs/components/stages_dropdown.vue' do + view 'app/assets/javascripts/jobs/components/job/sidebar/stages_dropdown.vue' do element :pipeline_path, required: true end - view 'app/assets/javascripts/jobs/components/sidebar.vue' do + view 'app/assets/javascripts/jobs/components/job/sidebar/legacy_sidebar_header.vue' do element :retry_button end - view 'app/assets/javascripts/jobs/components/artifacts_block.vue' do + view 'app/assets/javascripts/jobs/components/job/sidebar/artifacts_block.vue' do element :browse_artifacts_button end diff --git a/qa/qa/page/project/monitor/metrics/show.rb b/qa/qa/page/project/monitor/metrics/show.rb index 70ebc795595..59602d0fcf7 100644 --- a/qa/qa/page/project/monitor/metrics/show.rb +++ b/qa/qa/page/project/monitor/metrics/show.rb @@ -10,7 +10,7 @@ module QA LOADING_MESSAGE = 'Waiting for performance data' view 'app/assets/javascripts/monitoring/components/dashboard.vue' do - element :prometheus_graphs + element :prometheus_graphs_content end view 'app/assets/javascripts/monitoring/components/dashboard_header.vue' do @@ -54,7 +54,7 @@ module QA end def has_metrics? - within_element :prometheus_graphs do + within_element :prometheus_graphs_content do has_text?(EXPECTED_TITLE) end end @@ -102,7 +102,7 @@ module QA end def has_custom_metric?(metric) - within_element :prometheus_graphs do + within_element :prometheus_graphs_content do has_text?(metric) end end @@ -114,7 +114,7 @@ module QA end def has_template_metric?(metric) - within_element :prometheus_graphs do + within_element :prometheus_graphs_content do has_text?(metric) end end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index bb4fb84f2d2..fd650d8ca20 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -23,7 +23,7 @@ module QA view 'app/views/projects/project_templates/_template.html.haml' do element :use_template_button - element :template_option_row + element :template_option_container end view 'app/assets/javascripts/projects/new/components/new_project_url_select.vue' do diff --git a/qa/qa/page/project/packages/index.rb b/qa/qa/page/project/packages/index.rb index 86a86c05c12..e58ffba3cd5 100644 --- a/qa/qa/page/project/packages/index.rb +++ b/qa/qa/page/project/packages/index.rb @@ -6,7 +6,10 @@ module QA module Packages class Index < QA::Page::Base view 'app/assets/javascripts/packages_and_registries/package_registry/components/list/package_list_row.vue' do - element :package_row + element :package_link + end + + view 'app/assets/javascripts/packages_and_registries/infrastructure_registry/shared/package_list_row.vue' do element :package_link end @@ -18,6 +21,10 @@ module QA has_element?(:package_link, text: name, wait: 20) end + def has_module?(name) + has_element?(:package_link, text: name, wait: 20) + end + def has_no_package?(name) has_no_element?(:package_link, text: name) end diff --git a/qa/qa/page/project/pipeline/new.rb b/qa/qa/page/project/pipeline/new.rb index 6cf5c3b1134..742fcad5c07 100644 --- a/qa/qa/page/project/pipeline/new.rb +++ b/qa/qa/page/project/pipeline/new.rb @@ -5,7 +5,7 @@ module QA module Project module Pipeline class New < QA::Page::Base - view 'app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue' do + view 'app/assets/javascripts/pipeline_new/components/legacy_pipeline_new_form.vue' do element :run_pipeline_button, required: true element :ci_variable_row_container element :ci_variable_key_field diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index 52ed630ac66..ca5d13abdae 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -13,11 +13,14 @@ module QA view 'app/views/projects/edit.html.haml' do element :advanced_settings_content - element :merge_request_settings_content element :visibility_features_permissions_content element :badges_settings_content end + view 'app/views/projects/settings/merge_requests/show.html.haml' do + element :merge_request_settings_content + end + view 'app/views/projects/settings/_general.html.haml' do element :project_name_field element :save_naming_topics_avatar_button @@ -42,12 +45,6 @@ module QA end end - def expand_merge_requests_settings(&block) - expand_content(:merge_request_settings_content) do - MergeRequest.perform(&block) - end - end - def expand_visibility_project_features_permissions(&block) expand_content(:visibility_features_permissions_content) do VisibilityFeaturesPermissions.perform(&block) diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index dd9c94ebbb7..d862979aeec 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -7,7 +7,7 @@ module QA class MergeRequest < QA::Page::Base include QA::Page::Settings::Common - view 'app/views/projects/edit.html.haml' do + view 'app/views/projects/settings/merge_requests/show.html.haml' do element :save_merge_request_changes_button end diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb index 501b31f8a95..7eeeeefdae6 100644 --- a/qa/qa/page/project/settings/mirroring_repositories.rb +++ b/qa/qa/page/project/settings/mirroring_repositories.rb @@ -13,6 +13,9 @@ module QA view 'app/views/projects/mirrors/_mirror_repos.html.haml' do element :mirror_repository_url_input element :mirror_repository_button + end + + view 'app/views/projects/mirrors/_mirror_repos_list.html.haml' do element :mirror_repository_url_cell element :mirror_last_update_at_cell element :mirror_error_badge diff --git a/qa/qa/page/project/settings/pages.rb b/qa/qa/page/project/settings/pages.rb index 1417f7ec1b5..c5b8560ba9a 100644 --- a/qa/qa/page/project/settings/pages.rb +++ b/qa/qa/page/project/settings/pages.rb @@ -14,6 +14,7 @@ module QA def go_to_access_page within_element(:access_page_container) do find('a').click + page.driver.browser.switch_to.window(page.driver.browser.window_handles.last) end end end diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb index 35fc87f717c..a78d8a6ccf4 100644 --- a/qa/qa/page/project/settings/protected_branches.rb +++ b/qa/qa/page/project/settings/protected_branches.rb @@ -11,14 +11,10 @@ module QA end view 'app/views/projects/protected_branches/_create_protected_branch.html.haml' do - element :allowed_to_push_select element :allowed_to_push_dropdown - element :allowed_to_merge_select + element :allowed_to_push_dropdown_content element :allowed_to_merge_dropdown - end - - view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do - element :protected_branches_list + element :allowed_to_merge_dropdown_content end view 'app/views/projects/protected_branches/shared/_create_protected_branch.html.haml' do @@ -49,11 +45,11 @@ module QA private def select_allowed(action, allowed) - click_element :"allowed_to_#{action}_select" + click_element :"allowed_to_#{action}_dropdown" allowed[:roles] = Resource::ProtectedBranch::Roles::NO_ONE unless allowed.key?(:roles) - within_element(:"allowed_to_#{action}_dropdown") do + within_element(:"allowed_to_#{action}_dropdown_content") do click_on allowed[:roles][:description] allowed[:users].each { |user| click_on user.username } if allowed.key?(:users) allowed[:groups].each { |group| click_on group.name } if allowed.key?(:groups) diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index e048afee8b3..26c2da07b34 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -58,8 +58,8 @@ module QA end view 'app/assets/javascripts/repository/components/breadcrumbs.vue' do - element :add_to_tree - element :new_file_option + element :add_to_tree_dropdown + element :new_file_menu_item end view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do @@ -90,8 +90,8 @@ module QA end def create_new_file! - click_element :add_to_tree - click_element :new_file_option + click_element :add_to_tree_dropdown + click_element :new_file_menu_item end def fork_project diff --git a/qa/qa/page/project/sub_menus/packages.rb b/qa/qa/page/project/sub_menus/packages.rb index 88e2101a86d..9600540c5bc 100644 --- a/qa/qa/page/project/sub_menus/packages.rb +++ b/qa/qa/page/project/sub_menus/packages.rb @@ -23,12 +23,20 @@ module QA end end + def go_to_infrastructure_registry + hover_registry do + within_submenu do + click_link('Infrastructure Registry') + end + end + end + private def hover_registry within_sidebar do - scroll_to_element(:sidebar_menu_link, menu_item: 'Packages & Registries') - find_element(:sidebar_menu_link, menu_item: 'Packages & Registries').hover + scroll_to_element(:sidebar_menu_link, menu_item: 'Packages and registries') + find_element(:sidebar_menu_link, menu_item: 'Packages and registries').hover yield end diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb index e35828ecd6a..f9d55c0009c 100644 --- a/qa/qa/page/project/sub_menus/repository.rb +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -37,6 +37,14 @@ module QA end end + def go_to_repository_contributors + hover_repository do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Contributors') + end + end + end + private def hover_repository diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb index 53a5eaf60c5..2ed4c28afb7 100644 --- a/qa/qa/page/project/sub_menus/settings.rb +++ b/qa/qa/page/project/sub_menus/settings.rb @@ -77,6 +77,14 @@ module QA end end + def go_to_merge_request_settings + hover_settings do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Merge requests') + end + end + end + private def hover_settings diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index e572569e496..293fcd1e676 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -163,7 +163,7 @@ module QA def create_new_file_from_template(file_name, template) click_element(:new_file_button, Page::Component::WebIDE::Modal::CreateNewFile) - within_element(:template_list) do + within_element(:template_list_content) do click_on file_name rescue Capybara::ElementNotFound raise ElementNotFound, %Q(Couldn't find file template named "#{file_name}". Please confirm that it is a valid option.) diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index ba1b581b100..6025dd6ec40 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -12,6 +12,8 @@ module QA NoValueError = Class.new(RuntimeError) + attr_reader :retrieved_from_cache + class << self # Initialize new instance of class without fabrication # @@ -81,7 +83,7 @@ module QA Support::FabricationTracker.start_fabrication result = yield.tap do fabrication_time = Time.now - start - fabrication_http_method = if resource.api_fabrication_http_method == :get + fabrication_http_method = if resource.api_fabrication_http_method == :get || resource.retrieved_from_cache if include?(Reusable) "Retrieved for reuse" else @@ -92,24 +94,28 @@ module QA end Support::FabricationTracker.save_fabrication(:"#{fabrication_method}_fabrication", fabrication_time) - Tools::TestResourceDataProcessor.collect( - resource: resource, - info: resource.identifier, - fabrication_method: fabrication_method, - fabrication_time: fabrication_time - ) + + unless resource.retrieved_from_cache + Tools::TestResourceDataProcessor.collect( + resource: resource, + info: resource.identifier, + fabrication_method: fabrication_method, + fabrication_time: fabrication_time + ) + end Runtime::Logger.info do msg = ["==#{'=' * parents.size}>"] msg << "#{fabrication_http_method} a #{Rainbow(name).black.bg(:white)}" msg << resource.identifier msg << "as a dependency of #{parents.last}" if parents.any? - msg << "via #{fabrication_method}" + msg << "via #{resource.retrieved_from_cache ? 'cache' : fabrication_method}" msg << "in #{fabrication_time.round(2)} seconds" msg.compact.join(' ') end end + Support::FabricationTracker.finish_fabrication result @@ -263,6 +269,8 @@ module QA end def log_having_both_api_result_and_block(name, api_value) + api_value = "[MASKED]" if name == :token + 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 diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb index ef663bb613f..b632446623d 100644 --- a/qa/qa/resource/ci_variable.rb +++ b/qa/qa/resource/ci_variable.rb @@ -3,7 +3,7 @@ module QA module Resource class CiVariable < Base - attr_accessor :key, :value, :masked, :protected + attr_accessor :key, :value, :masked, :protected, :variable_type attribute :project do Project.fabricate! do |resource| @@ -15,6 +15,7 @@ module QA def initialize @masked = false @protected = false + @variable_type = 'env_var' end def fabricate! @@ -55,7 +56,8 @@ module QA key: key, value: value, masked: masked, - protected: protected + protected: protected, + variable_type: variable_type } end end diff --git a/qa/qa/resource/clusters/agent.rb b/qa/qa/resource/clusters/agent.rb index b190634f357..9574289a2ed 100644 --- a/qa/qa/resource/clusters/agent.rb +++ b/qa/qa/resource/clusters/agent.rb @@ -26,25 +26,18 @@ module QA end def api_get_path - "gid://gitlab/Clusters::Agent/#{id}" + "/projects/#{project.id}/cluster_agents/#{id}" end def api_post_path - "/graphql" + "/projects/#{project.id}/cluster_agents" end def api_post_body - <<~GQL - mutation createAgent { - createClusterAgent(input: { projectPath: "#{project.full_path}", name: "#{@name}" }) { - clusterAgent { - id - name - } - errors - } + { + id: project.id, + name: name } - GQL end end end diff --git a/qa/qa/resource/clusters/agent_token.rb b/qa/qa/resource/clusters/agent_token.rb index c1cf5c2f37b..cbd2964c31d 100644 --- a/qa/qa/resource/clusters/agent_token.rb +++ b/qa/qa/resource/clusters/agent_token.rb @@ -5,7 +5,7 @@ module QA module Clusters class AgentToken < QA::Resource::Base attribute :id - attribute :secret + attribute :token attribute :agent do QA::Resource::Clusters::Agent.fabricate_via_api! end @@ -20,26 +20,19 @@ module QA end def api_get_path - "gid://gitlab/Clusters::AgentToken/#{id}" + "/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens/#{id}" end def api_post_path - "/graphql" + "/projects/#{agent.project.id}/cluster_agents/#{agent.id}/tokens" end def api_post_body - <<~GQL - mutation createToken { - clusterAgentTokenCreate(input: { clusterAgentId: "gid://gitlab/Clusters::Agent/#{agent.id}" name: "token-#{agent.id}" }) { - secret # This is the value you need to use on the next step - token { - createdAt - id - } - errors - } + { + id: agent.project.id, + agent_id: agent.id, + name: agent.name } - GQL end end end diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb index 0e6dd626312..2016b1d948d 100644 --- a/qa/qa/resource/fork.rb +++ b/qa/qa/resource/fork.rb @@ -36,7 +36,7 @@ module QA def fabricate! populate(:upstream, :user) - namespace_path ||= user.name + namespace_path ||= user.username # Sign out as admin and sign is as the fork user Flow::Login.sign_in(as: user) diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb index 60f6cbdfc51..84416c0600d 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -26,6 +26,8 @@ module QA def initialize @description = "QA test run at #{Runtime::Namespace.time}" @require_two_factor_authentication = false + # Add visibility to enable create private group + @visibility = 'public' end def fabricate! @@ -39,6 +41,7 @@ module QA Page::Group::New.perform do |group_new| group_new.set_path(path) + group_new.set_visibility(@visibility) group_new.create_subgroup end @@ -73,7 +76,7 @@ module QA parent_id: sandbox.id, path: path, name: name, - visibility: 'public', + visibility: @visibility.downcase, require_two_factor_authentication: @require_two_factor_authentication, avatar: avatar } diff --git a/qa/qa/resource/group_access_token.rb b/qa/qa/resource/group_access_token.rb index 934348a1737..a1079b16971 100644 --- a/qa/qa/resource/group_access_token.rb +++ b/qa/qa/resource/group_access_token.rb @@ -8,30 +8,50 @@ module QA attr_writer :name attribute :id + attribute :user_id + attribute :expires_at + attribute :token + attribute :group do Group.fabricate! end - attribute :token do - Page::Group::Settings::AccessTokens.perform(&:created_access_token) + def api_get_path + "/groups/#{group.id}/access_tokens/#{id}" + rescue NoValueError + token = parse_body(api_get_from("/groups/#{group.id}/access_tokens")).find { |t| t[:name] == name } + + raise ResourceNotFoundError unless token + + @id = token[:id] + retry end - def api_get_path - "/groups/#{group.id}/access_tokens" + def identifier + "with name '#{name}', token's bot username '#{token_user[:username]}'" end def api_post_path - api_get_path + "/groups/#{group.id}/access_tokens" + end + + def api_user_path + "/users/#{user_id}" + end + + def token_user + parse_body(api_get_from(api_user_path)) end def name - @name || 'api-group-access-token' + @name ||= "api-group-access-token-#{Faker::Alphanumeric.alphanumeric(number: 8)}" end def api_post_body { name: name, - scopes: ["api"] + scopes: ["api"], + expires_at: expires_at.to_s } end @@ -51,6 +71,11 @@ module QA end end + # Expire in 2 days just in case the token is created just before midnight + def expires_at + @expires_at || Time.now.utc.to_date + 2 + end + def fabricate! Flow::Login.sign_in_unless_signed_in @@ -59,12 +84,14 @@ module QA Page::Group::Menu.perform(&:go_to_access_token_settings) Page::Group::Settings::AccessTokens.perform do |token_page| - token_page.fill_token_name(name || 'api-project-access-token') + token_page.fill_token_name(name) token_page.check_api - # Expire in 2 days just in case the token is created just before midnight - token_page.fill_expiry_date(Time.now.utc.to_date + 2) + token_page.fill_expiry_date(expires_at) token_page.click_create_token_button + self.token = token_page.created_access_token end + + reload! end end end diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb index bda72703906..f6d1aacca0a 100644 --- a/qa/qa/resource/group_base.rb +++ b/qa/qa/resource/group_base.rb @@ -14,7 +14,9 @@ module QA attributes :id, :runners_token, :name, - :full_path + :full_path, + # Add visibility to enable create private group + :visibility # Get group projects # diff --git a/qa/qa/resource/impersonation_token.rb b/qa/qa/resource/impersonation_token.rb index 3bd356b5e9b..99a9cd7bbab 100644 --- a/qa/qa/resource/impersonation_token.rb +++ b/qa/qa/resource/impersonation_token.rb @@ -22,7 +22,7 @@ module QA end def api_post_path - api_get_path + "/users/#{user.id}/impersonation_tokens" end def name diff --git a/qa/qa/resource/job.rb b/qa/qa/resource/job.rb index 96c502e567c..5b0dac9b2df 100644 --- a/qa/qa/resource/job.rb +++ b/qa/qa/resource/job.rb @@ -22,6 +22,10 @@ module QA "/projects/#{project.id}/jobs/#{id}" end + def api_trace_path + "#{api_get_path}/trace" + end + def api_post_path end @@ -30,6 +34,11 @@ module QA artifacts: artifacts } end + + # Job log + def trace + get(request_url(api_trace_path)) + end end end end diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb deleted file mode 100644 index 0443b26064e..00000000000 --- a/qa/qa/resource/kubernetes_cluster/project_cluster.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -module QA - module Resource - module KubernetesCluster - # TODO: This resource is currently broken, since one-click apps have been removed. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/333818 - class ProjectCluster < Base - attr_writer :cluster, - :install_ingress, :install_prometheus, :install_runner, :domain - - attribute :project do - Resource::Project.fabricate! - end - - attribute :ingress_ip do - @cluster.fetch_external_ip_for_ingress - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform( - &:go_to_infrastructure_kubernetes) - - Page::Project::Infrastructure::Kubernetes::Index.perform( - &:connect_existing_cluster) - - Page::Project::Infrastructure::Kubernetes::AddExisting.perform do |cluster_page| - cluster_page.set_cluster_name(@cluster.cluster_name) - cluster_page.set_api_url(@cluster.api_url) - cluster_page.set_ca_certificate(@cluster.ca_certificate) - cluster_page.set_token(@cluster.token) - cluster_page.uncheck_rbac! unless @cluster.rbac - cluster_page.add_cluster! - end - - Page::Project::Infrastructure::Kubernetes::Show.perform do |show| - if @install_ingress - ingress_ip - - show.set_domain("#{@ingress_ip}.nip.io") - show.save_domain - end - end - end - end - end - end -end diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb index ad0f183c603..33044774c59 100644 --- a/qa/qa/resource/personal_access_token.rb +++ b/qa/qa/resource/personal_access_token.rb @@ -5,12 +5,13 @@ require 'date' module QA module Resource class PersonalAccessToken < Base - attr_accessor :name + attr_writer :name # The user for which the personal access token is to be created # This *could* be different than the api_client.user or the api_user provided by the QA::Resource::ApiFabricator attr_writer :user + attribute :id attribute :token # Only Admins can create PAT via the API. @@ -41,13 +42,35 @@ module QA end def api_get_path - '/personal_access_tokens' + "/personal_access_tokens/#{id}" + rescue NoValueError + user.reload! unless user.id + + api_client = Runtime::API::Client.new(:gitlab, + is_new_session: false, + user: user, + personal_access_token: self.token) + request_url = Runtime::API::Request.new(api_client, + "/personal_access_tokens?user_id=#{user.id}", + per_page: '100').url + + token = auto_paginated_response(request_url).find { |t| t[:name] == name } + + raise ResourceNotFoundError unless token + + @id = token[:id] + retry + end + + def name + @name ||= "api-pat-#{user.username}-#{Faker::Alphanumeric.alphanumeric(number: 8)}" end def api_post_body { - name: name || 'api-test-token', - scopes: ["api"] + name: name, + scopes: ["api"], + expires_at: expires_at.to_s } end @@ -59,12 +82,20 @@ module QA def find_and_set_value @token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username) + @retrieved_from_cache = true if @token + + @token end def cache_token QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token end + # Expire in 2 days just in case the token is created just before midnight + def expires_at + @expires_at || Time.now.utc.to_date + 2 + end + def fabricate! return if find_and_set_value @@ -76,8 +107,7 @@ module QA Page::Profile::PersonalAccessTokens.perform do |token_page| token_page.fill_token_name(name || 'api-test-token') token_page.check_api - # Expire in 2 days just in case the token is created just before midnight - token_page.fill_expiry_date(Time.now.utc.to_date + 2) + token_page.fill_expiry_date(expires_at) token_page.click_create_token_button self.token = Page::Profile::PersonalAccessTokens.perform(&:created_access_token) diff --git a/qa/qa/resource/project_access_token.rb b/qa/qa/resource/project_access_token.rb index 58cb3e667c0..1ccf2103d1d 100644 --- a/qa/qa/resource/project_access_token.rb +++ b/qa/qa/resource/project_access_token.rb @@ -8,29 +8,50 @@ module QA attr_writer :name attribute :id + attribute :user_id + attribute :expires_at + attribute :token + attribute :project do Project.fabricate! end - attribute :token do - Page::Project::Settings::AccessTokens.perform(&:created_access_token) - end def api_get_path - "/projects/#{project.api_resource[:id]}/access_tokens" + "/projects/#{project.id}/access_tokens/#{id}" + rescue NoValueError + token = parse_body(api_get_from("/projects/#{project.id}/access_tokens")).find { |t| t[:name] == name } + + raise ResourceNotFoundError unless token + + @id = token[:id] + retry + end + + def identifier + "with name '#{name}', token's bot username '#{token_user[:username]}'" end def api_post_path - api_get_path + "/projects/#{project.id}/access_tokens" + end + + def api_user_path + "/users/#{user_id}" + end + + def token_user + parse_body(api_get_from(api_user_path)) end def name - @name || 'api-project-access-token' + @name ||= "api-project-access-token-#{Faker::Alphanumeric.alphanumeric(number: 8)}" end def api_post_body { name: name, - scopes: ["api"] + scopes: ["api"], + expires_at: expires_at.to_s } end @@ -50,6 +71,11 @@ module QA end end + # Expire in 2 days just in case the token is created just before midnight + def expires_at + @expires_at || Time.now.utc.to_date + 2 + end + def fabricate! Flow::Login.sign_in_unless_signed_in @@ -58,12 +84,14 @@ module QA Page::Project::Menu.perform(&:go_to_access_token_settings) Page::Project::Settings::AccessTokens.perform do |token_page| - token_page.fill_token_name(name || 'api-project-access-token') + token_page.fill_token_name(name) token_page.check_api - # Expire in 2 days just in case the token is created just before midnight - token_page.fill_expiry_date(Time.now.utc.to_date + 2) + token_page.fill_expiry_date(expires_at) token_page.click_create_token_button + self.token = token_page.created_access_token end + + reload! end end end diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb index 6d5ff71b2ba..da4021f89b7 100644 --- a/qa/qa/resource/runner.rb +++ b/qa/qa/resource/runner.rb @@ -32,7 +32,10 @@ module QA def fabricate_via_api! @docker_container = Service::DockerRun::GitlabRunner.new(name).tap do |runner| - runner.pull + QA::Support::Retrier.retry_on_exception(sleep_interval: 5) do + runner.pull + end + runner.token = @token ||= project.runners_token runner.address = Runtime::Scenario.gitlab_address runner.tags = @tags if @tags diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb index 12ecce1ce9a..2080e279e99 100644 --- a/qa/qa/resource/sandbox.rb +++ b/qa/qa/resource/sandbox.rb @@ -20,6 +20,8 @@ module QA def initialize @path = Runtime::Namespace.sandbox_name + # Visibility should be public by default for backward compatibility + @visibility = 'public' end alias_method :full_path, :path @@ -38,7 +40,7 @@ module QA Page::Group::New.perform do |group| group.click_create_group group.set_path(path) - group.set_visibility('Public') + group.set_visibility(@visibility) group.create end @@ -68,7 +70,7 @@ module QA { path: path, name: path, - visibility: 'public', + visibility: @visibility.downcase, avatar: avatar } end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index 2fb8b18b71f..a974446b3cb 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -44,7 +44,7 @@ module QA alias_method :ldap_username, :username def password - @password || 'password' + @password ||= SecureRandom.hex(8) end alias_method :ldap_password, :password @@ -232,3 +232,5 @@ module QA end end end + +QA::Resource::User.prepend_mod_with('Resource::User', namespace: QA) diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index f705fe54b97..0dbc3cdf09d 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -95,6 +95,14 @@ module QA # Disable /dev/shm use in CI. See https://gitlab.com/gitlab-org/gitlab/issues/4252 capabilities['goog:chromeOptions'][:args] << 'disable-dev-shm-usage' if QA::Runtime::Env.disable_dev_shm? + # Set chrome default download path + if QA::Runtime::Env.chrome_default_download_path + capabilities['goog:chromeOptions'][:prefs] = { + 'download.default_directory' => File.expand_path(QA::Runtime::Env.chrome_default_download_path), + 'download.prompt_for_download' => false + } + end + # Specify the user-agent to allow challenges to be bypassed # See https://gitlab.com/gitlab-com/gl-infra/infrastructure/-/issues/11938 if QA::Runtime::Env.user_agent diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 1fd097d0acf..b1a912ac43e 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -263,22 +263,6 @@ module QA ENV['GITLAB_QA_PASSWORD_6'] end - def gitlab_qa_1p_email - ENV['GITLAB_QA_1P_EMAIL'] - end - - def gitlab_qa_1p_password - ENV['GITLAB_QA_1P_PASSWORD'] - end - - def gitlab_qa_1p_secret - ENV['GITLAB_QA_1P_SECRET'] - end - - def gitlab_qa_1p_github_uuid - ENV['GITLAB_QA_1P_GITHUB_UUID'] - end - def jira_admin_username ENV['JIRA_ADMIN_USERNAME'] end @@ -496,6 +480,10 @@ module QA enabled?(ENV['QA_USE_PUBLIC_IP_API'], default: false) end + def chrome_default_download_path + ENV['DEFAULT_CHROME_DOWNLOAD_PATH'] + end + private def remote_grid_credentials diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb index 41d7ce5d178..d56af9b4872 100644 --- a/qa/qa/runtime/fixtures.rb +++ b/qa/qa/runtime/fixtures.rb @@ -49,3 +49,5 @@ module QA end end end + +QA::Runtime::Fixtures.prepend_mod_with('Runtime::Fixtures', namespace: QA) diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb index e4eeca2000f..cb2a86276f9 100644 --- a/qa/qa/runtime/user.rb +++ b/qa/qa/runtime/user.rb @@ -6,7 +6,10 @@ module QA extend self def admin - Struct.new(:username, :password).new(admin_username, admin_password) + QA::Resource::User.init do |user| + user.username = admin_username + user.password = admin_password + end end def default_username diff --git a/qa/qa/scenario/test/instance/cloud_activation.rb b/qa/qa/scenario/test/instance/cloud_activation.rb new file mode 100644 index 00000000000..0f61cab582a --- /dev/null +++ b/qa/qa/scenario/test/instance/cloud_activation.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class CloudActivation < All + tags :cloud_activation + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/integrations.rb b/qa/qa/scenario/test/instance/integrations.rb new file mode 100644 index 00000000000..b943b95c51e --- /dev/null +++ b/qa/qa/scenario/test/instance/integrations.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class Integrations < All + tags :integrations + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/jira.rb b/qa/qa/scenario/test/instance/jira.rb new file mode 100644 index 00000000000..784a71515cb --- /dev/null +++ b/qa/qa/scenario/test/instance/jira.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class Jira < All + tags :jira + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/large_setup.rb b/qa/qa/scenario/test/instance/large_setup.rb new file mode 100644 index 00000000000..934c841c619 --- /dev/null +++ b/qa/qa/scenario/test/instance/large_setup.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class LargeSetup < All + tags :can_use_large_setup + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/metrics.rb b/qa/qa/scenario/test/instance/metrics.rb new file mode 100644 index 00000000000..7643aff095b --- /dev/null +++ b/qa/qa/scenario/test/instance/metrics.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class Metrics < All + tags :metrics + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/object_storage.rb b/qa/qa/scenario/test/instance/object_storage.rb new file mode 100644 index 00000000000..6d54b50a505 --- /dev/null +++ b/qa/qa/scenario/test/instance/object_storage.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class ObjectStorage < All + tags :object_storage + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/packages.rb b/qa/qa/scenario/test/instance/packages.rb new file mode 100644 index 00000000000..7648a2b5e80 --- /dev/null +++ b/qa/qa/scenario/test/instance/packages.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class Packages < All + tags :packages + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/repository_storage.rb b/qa/qa/scenario/test/instance/repository_storage.rb new file mode 100644 index 00000000000..c648335c353 --- /dev/null +++ b/qa/qa/scenario/test/instance/repository_storage.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class RepositoryStorage < All + tags :repository_storage + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/review_blocking.rb b/qa/qa/scenario/test/instance/review_blocking.rb new file mode 100644 index 00000000000..cb1b6c9cf10 --- /dev/null +++ b/qa/qa/scenario/test/instance/review_blocking.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class ReviewBlocking < All + tags :reliable, + :sanity_feature_flags, + :"~orchestrated", + :"~skip_signup_disabled" + end + end + end + end +end diff --git a/qa/qa/scenario/test/instance/review_non_blocking.rb b/qa/qa/scenario/test/instance/review_non_blocking.rb new file mode 100644 index 00000000000..e295171eb0b --- /dev/null +++ b/qa/qa/scenario/test/instance/review_non_blocking.rb @@ -0,0 +1,16 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Instance + class ReviewNonBlocking < All + tags :"~reliable", + :"~smoke", + :"~skip_signup_disabled", + *Specs::Runner::DEFAULT_SKIPPED_TAGS.map { |tag| :"~#{tag}" } + end + end + end + end +end diff --git a/qa/qa/service/cluster_provider/gcloud.rb b/qa/qa/service/cluster_provider/gcloud.rb index 77677745f7a..14c13eecb8d 100644 --- a/qa/qa/service/cluster_provider/gcloud.rb +++ b/qa/qa/service/cluster_provider/gcloud.rb @@ -33,14 +33,32 @@ module QA delete_cluster end - def install_ingress - QA::Runtime::Logger.info "Attempting to install Ingress on cluster #{cluster_name}" - shell 'kubectl create -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-0.31.0/deploy/static/provider/cloud/deploy.yaml' - wait_for_ingress + # kas is hardcoded to staging since this test should only run in staging for now + def install_kubernetes_agent(agent_token) + install_helm + + shell <<~CMD.tr("\n", ' ') + helm repo add gitlab https://charts.gitlab.io && + helm repo update && + helm upgrade --install test gitlab/gitlab-agent + --namespace gitlab-agent + --create-namespace + --set image.tag=#{Runtime::Env.gitlab_agentk_version} + --set config.token=#{agent_token} + --set config.kasAddress=wss://kas.staging.gitlab.com + CMD end private + def install_helm + shell <<~CMD.tr("\n", ' ') + curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 && + chmod 700 get_helm.sh && + ./get_helm.sh + CMD + end + def login_if_not_already_logged_in if Runtime::Env.has_gcloud_credentials? attempt_login_with_env_vars @@ -104,18 +122,6 @@ module QA def get_region Runtime::Env.gcloud_region || @available_regions.delete(@available_regions.sample) end - - def wait_for_ingress - QA::Runtime::Logger.info 'Waiting for Ingress controller pod to be initialized' - - Support::Retrier.retry_until(max_attempts: 60, sleep_interval: 1) do - service_available?('kubectl get pods --all-namespaces -l app.kubernetes.io/component=controller | grep -o "ingress-nginx-controller.*1/1"') - end - end - - def service_available?(command) - system("#{command} > /dev/null 2>&1") - end end end end diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb index dafce4acc33..59bfacf9195 100644 --- a/qa/qa/service/kubernetes_cluster.rb +++ b/qa/qa/service/kubernetes_cluster.rb @@ -41,8 +41,8 @@ module QA cluster_name end - def install_ingress - @provider.install_ingress + def install_kubernetes_agent(agent_token) + @provider.install_kubernetes_agent(agent_token) end def create_secret(secret, secret_name) @@ -73,16 +73,6 @@ module QA shell('kubectl apply -f -', stdin_data: network_policy) end - def fetch_external_ip_for_ingress - install_ingress - - # need to wait since the ingress-nginx service has an initial delay set of 10 seconds - sleep 12 - ingress_ip = `kubectl get svc --all-namespaces --no-headers=true -l app.kubernetes.io/name=ingress-nginx -o custom-columns=:'status.loadBalancer.ingress[0].ip' | grep -v 'none'` - QA::Runtime::Logger.debug "Has ingress address set to: #{ingress_ip}" - ingress_ip - end - private def fetch_api_url diff --git a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/automatic_failover_and_recovery_spec.rb index 55ae0d215cf..2058d58d5d6 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb +++ b/qa/qa/specs/features/api/12_systems/gitaly/automatic_failover_and_recovery_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do - context 'Gitaly automatic failover and recovery', :orchestrated, :gitaly_cluster do + RSpec.describe 'Systems' do + context 'with Gitaly automatic failover and recovery', :orchestrated, :gitaly_cluster do # Variables shared between contexts. They're used and shared between # contexts so they can't be `let` variables. praefect_manager = Service::PraefectManager.new @@ -47,11 +47,11 @@ module QA commit.project = project commit.commit_message = second_added_commit_message commit.add_files([ - { - file_path: "file-#{SecureRandom.hex(8)}", - content: 'This is created on gitaly2/gitaly3 while gitaly1 is unavailable' - } - ]) + { + file_path: "file-#{SecureRandom.hex(8)}", + content: 'This is created on gitaly2/gitaly3 while gitaly1 is unavailable' + } + ]) end # Confirm that we have access to the repo after failover, diff --git a/qa/qa/specs/features/api/12_systems/gitaly/backend_node_recovery_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/backend_node_recovery_spec.rb new file mode 100644 index 00000000000..0b4bdf550f8 --- /dev/null +++ b/qa/qa/specs/features/api/12_systems/gitaly/backend_node_recovery_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Systems' do + describe 'Gitaly backend node recovery', :orchestrated, :gitaly_cluster, :skip_live_env do + let(:praefect_manager) { Service::PraefectManager.new } + let(:project) do + Resource::Project.fabricate! do |project| + project.name = "gitaly_cluster" + project.initialize_with_readme = true + end + end + + before do + # Reset the cluster in case previous tests left it in a bad state + praefect_manager.start_all_nodes + end + + after do + # Leave the cluster in a suitable state for subsequent tests + praefect_manager.start_all_nodes + end + + it 'recovers from dataloss', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347832' do + # Create a new project with a commit and wait for it to replicate + praefect_manager.wait_for_replication(project.id) + + # Stop the primary node to trigger failover, and then wait + # for Gitaly to be ready for writes again + praefect_manager.stop_primary_node + praefect_manager.wait_for_primary_node_health_check_failure + + # Push a commit to the new primary + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.new_branch = false + push.commit_message = 'pushed after failover' + push.file_name = 'new_file' + push.file_content = 'new file' + end + + # Confirm that the commit is waiting to be replicated + expect(praefect_manager).to be_replication_pending + + # Start the old primary node again + praefect_manager.start_primary_node + praefect_manager.wait_for_health_check_all_nodes + + # Wait for automatic replication + praefect_manager.wait_for_replication(project.id) + + # Force switch to the old primary node + # This ensures that the commit was replicated + praefect_manager.stop_secondary_node + praefect_manager.stop_tertiary_node + + # Confirm that both commits are available + expect(project.commits.map { |commit| commit[:message].chomp }) + .to include("Initial commit").and include("pushed after failover") + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/changing_repository_storage_spec.rb index 5ee6dfdb8d8..18ec8e0a8b4 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb +++ b/qa/qa/specs/features/api/12_systems/gitaly/changing_repository_storage_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Systems' do describe 'Changing Gitaly repository storage', :requires_admin, except: { job: 'review-qa-*' } do praefect_manager = Service::PraefectManager.new @@ -9,7 +9,8 @@ module QA it 'confirms a `finished` status after moving project repository storage' do expect(project).to have_file('README.md') expect { project.change_repository_storage(destination_storage[:name]) }.not_to raise_error - expect { praefect_manager.verify_storage_move(source_storage, destination_storage, repo_type: :project) }.not_to raise_error + expect { praefect_manager.verify_storage_move(source_storage, destination_storage, repo_type: :project) } + .not_to raise_error Support::Retrier.retry_on_exception(sleep_interval: 5) do # For a short period of time after migrating, the repository can be 'read only' which may lead to errors @@ -18,8 +19,8 @@ module QA commit.project = project commit.commit_message = 'Add new file' commit.add_files([ - { file_path: 'new_file', content: '# This is a new file' } - ]) + { file_path: 'new_file', content: '# This is a new file' } + ]) end end @@ -28,7 +29,8 @@ module QA end end - context 'when moving from one Gitaly storage to another', :orchestrated, :repository_storage, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347827' do + context 'when moving from one Gitaly storage to another', :orchestrated, :repository_storage, + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347827' do let(:source_storage) { { type: :gitaly, name: 'default' } } let(:destination_storage) { { type: :gitaly, name: QA::Runtime::Env.additional_repository_storage } } let(:project) do @@ -49,7 +51,8 @@ module QA # Note: This test doesn't have the :orchestrated tag because it runs in the Test::Integration::Praefect # scenario with other tests that aren't considered orchestrated. # It also runs on staging using nfs-file07 as non-cluster storage and nfs-file22 as cluster/praefect storage - context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347828' do + context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347828' do let(:source_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } } let(:destination_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } } let(:project) do @@ -71,7 +74,8 @@ module QA # Note: This test doesn't have the :orchestrated tag because it runs in the Test::Integration::Praefect # scenario with other tests that aren't considered orchestrated. # It also runs on staging using nfs-file07 as non-cluster storage and nfs-file22 as cluster/praefect storage - context 'when moving from Gitaly Cluster to Gitaly', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/369204' do + context 'when moving from Gitaly Cluster to Gitaly', :requires_praefect, + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/369204' do let(:source_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } } let(:destination_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } } let(:project) do diff --git a/qa/qa/specs/features/api/12_systems/gitaly/distributed_reads_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/distributed_reads_spec.rb new file mode 100644 index 00000000000..692297e40ce --- /dev/null +++ b/qa/qa/specs/features/api/12_systems/gitaly/distributed_reads_spec.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'parallel' + +module QA + RSpec.describe 'Systems' do + describe 'Gitaly distributed reads', :orchestrated, :gitaly_cluster, :skip_live_env, :requires_admin do + let(:number_of_reads_per_loop) { 9 } + let(:praefect_manager) { Service::PraefectManager.new } + let(:project) do + Resource::Project.fabricate! do |project| + project.name = "gitaly_cluster" + project.initialize_with_readme = true + end + end + + before do + praefect_manager.start_all_nodes + praefect_manager.wait_for_replication(project.id) + end + + it 'reads from each node', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347833' do + pre_read_data = praefect_manager.query_read_distribution + + wait_for_reads_to_increase(project, number_of_reads_per_loop, pre_read_data) + + aggregate_failures "each gitaly node" do + praefect_manager.query_read_distribution.each_with_index do |data, index| + pre_read_count = praefect_manager.value_for_node(pre_read_data, data[:node]) + QA::Runtime::Logger.debug("Node: #{data[:node]}; before: #{pre_read_count}; now: #{data[:value]}") + expect(data[:value]).to be > pre_read_count, + "Read counts did not differ for node #{data[:node]}" + end + end + end + + context 'when a node is unhealthy' do + before do + praefect_manager.stop_secondary_node + end + + after do + # Leave the cluster in a suitable state for subsequent tests + praefect_manager.start_secondary_node + end + + it 'does not read from the unhealthy node', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347834' do + pre_read_data = praefect_manager.query_read_distribution + + read_from_project(project, number_of_reads_per_loop * 10) + + praefect_manager.wait_for_read_count_change(pre_read_data) + + post_read_data = praefect_manager.query_read_distribution + + aggregate_failures "each gitaly node" do + expect(praefect_manager.value_for_node(post_read_data, 'gitaly1')) + .to be > praefect_manager.value_for_node(pre_read_data, 'gitaly1') + expect(praefect_manager.value_for_node(post_read_data, 'gitaly2')) + .to eq praefect_manager.value_for_node(pre_read_data, 'gitaly2') + expect(praefect_manager.value_for_node(post_read_data, 'gitaly3')) + .to be > praefect_manager.value_for_node(pre_read_data, 'gitaly3') + end + end + end + + def read_from_project(project, number_of_reads) + QA::Runtime::Logger.info('Reading from the repository') + Parallel.each((1..number_of_reads)) do + Git::Repository.perform do |repository| + repository.uri = project.repository_http_location.uri + repository.use_default_credentials + repository.clone + end + end + end + + def wait_for_reads_to_increase(project, number_of_reads, pre_read_data) + diff_found = pre_read_data + + Support::Waiter.wait_until(sleep_interval: 5, raise_on_failure: false) do + read_from_project(project, number_of_reads) + + praefect_manager.query_read_distribution.each_with_index do |data, index| + diff_found[index] = {} unless diff_found[index] + if data[:value] > praefect_manager.value_for_node(pre_read_data, data[:node]) + diff_found[index][:diff] = true + end + end + diff_found.all? { |node| node.key?(:diff) && node[:diff] } + end + end + end + end +end diff --git a/qa/qa/specs/features/api/12_systems/gitaly/gitaly_mtls_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/gitaly_mtls_spec.rb new file mode 100644 index 00000000000..a4b39554453 --- /dev/null +++ b/qa/qa/specs/features/api/12_systems/gitaly/gitaly_mtls_spec.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Systems' do + describe 'Gitaly using mTLS', :orchestrated, :mtls do + let(:intial_commit_message) { 'Initial commit' } + let(:first_added_commit_message) { 'commit over git' } + let(:second_added_commit_message) { 'commit over api' } + + it 'pushes to gitaly', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347677' do + project = Resource::Project.fabricate! do |project| + project.name = "mTLS" + project.initialize_with_readme = true + end + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.new_branch = false + push.commit_message = first_added_commit_message + push.file_content = 'First commit' + end + + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = second_added_commit_message + commit.add_files( + [{ + file_path: "file-#{SecureRandom.hex(8)}", + content: 'Second commit' + }] + ) + end + + expect(project.commits.map { |commit| commit[:message].chomp }) + .to include(intial_commit_message) + .and include(first_added_commit_message) + .and include(second_added_commit_message) + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_connectivity_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/praefect_connectivity_spec.rb index 28469b99d04..f25b50f584d 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_connectivity_spec.rb +++ b/qa/qa/specs/features/api/12_systems/gitaly/praefect_connectivity_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do - context 'Praefect connectivity commands', :orchestrated, :gitaly_cluster do + RSpec.describe 'Systems' do + describe 'Praefect connectivity commands', :orchestrated, :gitaly_cluster do praefect_manager = Service::PraefectManager.new before do @@ -10,22 +10,26 @@ module QA end context 'in a healthy environment' do - it 'confirms healthy connection to database', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349937' do + it 'confirms healthy connection to database', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349937' do expect(praefect_manager.praefect_sql_ping_healthy?).to be true end - it 'confirms healthy connection to gitaly nodes', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349938' do + it 'confirms healthy connection to gitaly nodes', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349938' do expect(praefect_manager.wait_for_dial_nodes_successful).to be true end end context 'in an unhealthy environment' do - it 'diagnoses unhealthy connection to database', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349939' do + it 'diagnoses unhealthy connection to database', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349939' do praefect_manager.stop_node(praefect_manager.postgres) expect(praefect_manager.praefect_sql_ping_healthy?).to be false end - it 'diagnoses connection issues to gitaly nodes', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349940' do + it 'diagnoses connection issues to gitaly nodes', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349940' do praefect_manager.stop_node(praefect_manager.primary_node) praefect_manager.stop_node(praefect_manager.tertiary_node) expect(praefect_manager.praefect_dial_nodes_status?(praefect_manager.primary_node, false)).to be true diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/praefect_dataloss_spec.rb index 5b02cc4646c..944c58ebc83 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb +++ b/qa/qa/specs/features/api/12_systems/gitaly/praefect_dataloss_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do - context 'Praefect dataloss commands', :orchestrated, :gitaly_cluster do + RSpec.describe 'Systems' do + describe 'Praefect dataloss commands', :orchestrated, :gitaly_cluster do let(:praefect_manager) { Service::PraefectManager.new } let(:project) do @@ -16,13 +16,15 @@ module QA praefect_manager.start_all_nodes end - it 'confirms that changes are synced across all storages', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352691' do + it 'confirms that changes are synced across all storages', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352691' do expect { praefect_manager.praefect_dataloss_information(project.id) } .to(eventually_include('All repositories are fully available on all assigned storages!') .within(max_duration: 60)) end - it 'identifies how many changes are not in sync across storages', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352692' do + it 'identifies how many changes are not in sync across storages', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352692' do # Ensure our test repository is replicated and in a consistent state prior to test praefect_manager.wait_for_project_synced_across_all_storages(project.id) @@ -36,9 +38,11 @@ module QA commit.branch = "newbranch-#{SecureRandom.hex(8)}" commit.start_branch = project.default_branch commit.commit_message = 'Add new file' - commit.add_files([ - { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'new file' } - ]) + commit.add_files( + [{ + file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'new file' + }] + ) end end @@ -48,21 +52,25 @@ module QA project_data_loss = praefect_manager.praefect_dataloss_information(project.id) aggregate_failures "validate dataloss identified" do expect(project_data_loss).to include('gitaly1, assigned host') - expect(project_data_loss).to include("gitaly2 is behind by #{number_of_changes} changes or less, assigned host, unhealthy") + expect(project_data_loss) + .to include("gitaly2 is behind by #{number_of_changes} changes or less, assigned host, unhealthy") expect(project_data_loss).to include('gitaly3, assigned host, unhealthy') end end - it 'allows admin resolve scenario where data cannot be recovered', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352708' do + it 'allows admin resolve scenario where data cannot be recovered', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352708' do # Ensure everything is in sync before begining test praefect_manager.wait_for_project_synced_across_all_storages(project.id) Resource::Repository::Commit.fabricate_via_api! do |commit| commit.project = project commit.commit_message = 'accept-dataloss-1' - commit.add_files([ - { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly1,gitaly2,gitaly3' } - ]) + commit.add_files( + [{ + file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly1,gitaly2,gitaly3' + }] + ) end praefect_manager.wait_for_replication_to_node(project.id, praefect_manager.primary_node) @@ -70,9 +78,11 @@ module QA Resource::Repository::Commit.fabricate_via_api! do |commit| commit.project = project commit.commit_message = 'accept-dataloss-2' - commit.add_files([ - { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly2,gitaly3' } - ]) + commit.add_files( + [{ + file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly2,gitaly3' + }] + ) end praefect_manager.wait_for_replication_to_node(project.id, praefect_manager.secondary_node) @@ -81,8 +91,8 @@ module QA commit.project = project commit.commit_message = 'accept-dataloss-3' commit.add_files([ - { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly3' } - ]) + { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly3' } + ]) end # Confirms that they want to accept dataloss, using gitaly2 as authoritative storage to use as a base diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/praefect_replication_queue_spec.rb index a53614960cd..f4efcf74956 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb +++ b/qa/qa/specs/features/api/12_systems/gitaly/praefect_replication_queue_spec.rb @@ -3,8 +3,8 @@ require 'parallel' module QA - RSpec.describe 'Create' do - context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do + RSpec.describe 'Systems' do + describe 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do let(:praefect_manager) { Service::PraefectManager.new } let(:project) do Resource::Project.fabricate! do |project| @@ -22,7 +22,8 @@ module QA praefect_manager.clear_replication_queue end - it 'allows replication of different repository after interruption', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347829' do + it 'allows replication of different repository after interruption', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347829' do # We want to fill the replication queue with 10 `in_progress` jobs, # while a lock has been acquired, which is when the problem occurred # as reported in https://gitlab.com/gitlab-org/gitaly/-/issues/2801 diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb b/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb index 47be7e0620b..064743ae469 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb +++ b/qa/qa/specs/features/api/12_systems/gitaly/praefect_repo_sync_spec.rb @@ -1,12 +1,17 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do - context 'Praefect repository commands', :orchestrated, :gitaly_cluster do + RSpec.describe 'Systems' do + describe 'Praefect repository commands', :orchestrated, :gitaly_cluster do let(:praefect_manager) { Service::PraefectManager.new } - let(:repo1) { { "relative_path" => "@hashed/repo1.git", "storage" => "gitaly1", "virtual_storage" => "default" } } - let(:repo2) { { "relative_path" => "@hashed/path/to/repo2.git", "storage" => "gitaly3", "virtual_storage" => "default" } } + let(:repo1) do + { "relative_path" => "@hashed/repo1.git", "storage" => "gitaly1", "virtual_storage" => "default" } + end + + let(:repo2) do + { "relative_path" => "@hashed/path/to/repo2.git", "storage" => "gitaly3", "virtual_storage" => "default" } + end before do praefect_manager.start_all_nodes @@ -21,54 +26,70 @@ module QA praefect_manager.remove_repository_from_praefect_database(repo2["relative_path"]) end - it 'allows admin to manage difference between praefect database and disk state', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347606' do + it 'allows admin to manage difference between praefect database and disk state', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347606' do # Some repos are on disk that praefect is not aware of untracked_repositories = praefect_manager.list_untracked_repositories expect(untracked_repositories).to include(repo1) expect(untracked_repositories).to include(repo2) # admin manually adds the first repo to the praefect database - praefect_manager.track_repository_in_praefect(repo1["relative_path"], repo1["storage"], repo1["virtual_storage"]) + praefect_manager + .track_repository_in_praefect(repo1["relative_path"], repo1["storage"], repo1["virtual_storage"]) untracked_repositories = praefect_manager.list_untracked_repositories expect(untracked_repositories).not_to include(repo1) expect(untracked_repositories).to include(repo2) - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])).to be true + expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])) + .to be true expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be true # admin manually adds the second repo to the praefect database - praefect_manager.track_repository_in_praefect(repo2["relative_path"], repo2["storage"], repo2["virtual_storage"]) + praefect_manager + .track_repository_in_praefect(repo2["relative_path"], repo2["storage"], repo2["virtual_storage"]) untracked_repositories = praefect_manager.list_untracked_repositories expect(untracked_repositories).not_to include(repo2) - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo2["relative_path"])).to be true + expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo2["relative_path"])) + .to be true expect(praefect_manager.praefect_database_tracks_repo?(repo2["relative_path"])).to be true # admin ensures replication to other nodes occurs - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo1["relative_path"])).to be true - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.tertiary_node, repo1["relative_path"])).to be true - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.primary_node, repo2["relative_path"])).to be true - expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo2["relative_path"])).to be true + expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo1["relative_path"])) + .to be true + expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.tertiary_node, repo1["relative_path"])) + .to be true + expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.primary_node, repo2["relative_path"])) + .to be true + expect(praefect_manager.repository_replicated_to_disk?(praefect_manager.secondary_node, repo2["relative_path"])) + .to be true # admin chooses to remove the first repo completely from praefect and disk praefect_manager.remove_tracked_praefect_repository(repo1["relative_path"], repo1["virtual_storage"]) - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])).to be false - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.secondary_node, repo1["relative_path"])).to be false - expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo1["relative_path"])).to be false + expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.primary_node, repo1["relative_path"])) + .to be false + expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager + .secondary_node, repo1["relative_path"])).to be false + expect(praefect_manager.repository_exists_on_node_disk?(praefect_manager.tertiary_node, repo1["relative_path"])) + .to be false expect(praefect_manager.praefect_database_tracks_repo?(repo1["relative_path"])).to be false untracked_repositories = praefect_manager.list_untracked_repositories expect(untracked_repositories).not_to include(repo1) end - it 'allows admin to control the number of replicas of data', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347566' do - praefect_manager.track_repository_in_praefect(repo1['relative_path'], repo1['storage'], repo1['virtual_storage']) + it 'allows admin to control the number of replicas of data', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347566' do + praefect_manager + .track_repository_in_praefect(repo1['relative_path'], repo1['storage'], repo1['virtual_storage']) praefect_manager.set_replication_factor(repo1['relative_path'], repo1['virtual_storage'], 2) - replication_storages = praefect_manager.get_replication_storages(repo1['relative_path'], repo1['virtual_storage']) + replication_storages = praefect_manager + .get_replication_storages(repo1['relative_path'], repo1['virtual_storage']) expect(replication_storages).to have_attributes(size: 2) praefect_manager.set_replication_factor(repo1['relative_path'], repo1['virtual_storage'], 3) - replication_storages = praefect_manager.get_replication_storages(repo1['relative_path'], repo1['virtual_storage']) - expect(replication_storages).to eq(%w(gitaly1 gitaly2 gitaly3)) + replication_storages = praefect_manager + .get_replication_storages(repo1['relative_path'], repo1['virtual_storage']) + expect(replication_storages).to eq(%w[gitaly1 gitaly2 gitaly3]) end end end diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb index de460a39ccf..e6b60a5b090 100644 --- a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb @@ -88,14 +88,19 @@ module QA let(:gh_issue_comments) do logger.debug("= Fetching issue comments =") 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&.gsub(gh_link_pattern, dummy_url) # use base html url as key + # use base html url as key + hash[c.html_url.gsub(/\#\S+/, "")] << c.body&.gsub(gh_link_pattern, dummy_url) end end let(:gh_pr_comments) do logger.debug("= Fetching pr comments =") 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&.gsub(gh_link_pattern, dummy_url) # use base html url as key + # use base html url as key + hash[c.html_url.gsub(/\#\S+/, "")] << c.body + # some suggestions can contain extra whitespaces which gitlab will remove + &.gsub(/suggestion\s+\r/, "suggestion\r") + &.gsub(gh_link_pattern, dummy_url) end end diff --git a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb index 444d86f63d3..9f0e2664213 100644 --- a/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb +++ b/qa/qa/specs/features/api/1_manage/user_inherited_access_spec.rb @@ -71,7 +71,12 @@ module QA it( 'is allowed to commit to sub-group project via the API', :reliable, - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363349' + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363349', + quarantine: { + only: { subdomain: %i[staging staging-ref] }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/370282' + } ) do expect do Resource::Repository::Commit.fabricate_via_api! do |commit| diff --git a/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb deleted file mode 100644 index 25e860b4f6d..00000000000 --- a/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Create' do - context 'Gitaly' do - describe 'Backend node recovery', :orchestrated, :gitaly_cluster, :skip_live_env do - let(:praefect_manager) { Service::PraefectManager.new } - let(:project) do - Resource::Project.fabricate! do |project| - project.name = "gitaly_cluster" - project.initialize_with_readme = true - end - end - - before do - # Reset the cluster in case previous tests left it in a bad state - praefect_manager.start_all_nodes - end - - after do - # Leave the cluster in a suitable state for subsequent tests - praefect_manager.start_all_nodes - end - - it 'recovers from dataloss', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347832' do - # Create a new project with a commit and wait for it to replicate - praefect_manager.wait_for_replication(project.id) - - # Stop the primary node to trigger failover, and then wait - # for Gitaly to be ready for writes again - praefect_manager.stop_primary_node - praefect_manager.wait_for_primary_node_health_check_failure - - # Push a commit to the new primary - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.new_branch = false - push.commit_message = 'pushed after failover' - push.file_name = 'new_file' - push.file_content = 'new file' - end - - # Confirm that the commit is waiting to be replicated - expect(praefect_manager).to be_replication_pending - - # Start the old primary node again - praefect_manager.start_primary_node - praefect_manager.wait_for_health_check_all_nodes - - # Wait for automatic replication - praefect_manager.wait_for_replication(project.id) - - # Force switch to the old primary node - # This ensures that the commit was replicated - praefect_manager.stop_secondary_node - praefect_manager.stop_tertiary_node - - # Confirm that both commits are available - expect(project.commits.map { |commit| commit[:message].chomp }) - .to include("Initial commit").and include("pushed after failover") - end - end - end - end -end diff --git a/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb deleted file mode 100644 index 2b96c35415e..00000000000 --- a/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb +++ /dev/null @@ -1,92 +0,0 @@ -# frozen_string_literal: true - -require 'parallel' - -module QA - RSpec.describe 'Create' do - context 'Gitaly' do - describe 'Distributed reads', :orchestrated, :gitaly_cluster, :skip_live_env, :requires_admin do - let(:number_of_reads_per_loop) { 9 } - let(:praefect_manager) { Service::PraefectManager.new } - let(:project) do - Resource::Project.fabricate! do |project| - project.name = "gitaly_cluster" - project.initialize_with_readme = true - end - end - - before do - praefect_manager.start_all_nodes - praefect_manager.wait_for_replication(project.id) - end - - it 'reads from each node', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347833' do - pre_read_data = praefect_manager.query_read_distribution - - wait_for_reads_to_increase(project, number_of_reads_per_loop, pre_read_data) - - aggregate_failures "each gitaly node" do - praefect_manager.query_read_distribution.each_with_index do |data, index| - pre_read_count = praefect_manager.value_for_node(pre_read_data, data[:node]) - QA::Runtime::Logger.debug("Node: #{data[:node]}; before: #{pre_read_count}; now: #{data[:value]}") - expect(data[:value]).to be > pre_read_count, - "Read counts did not differ for node #{data[:node]}" - end - end - end - - context 'when a node is unhealthy' do - before do - praefect_manager.stop_secondary_node - end - - after do - # Leave the cluster in a suitable state for subsequent tests - praefect_manager.start_secondary_node - end - - it 'does not read from the unhealthy node', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347834' do - pre_read_data = praefect_manager.query_read_distribution - - read_from_project(project, number_of_reads_per_loop * 10) - - praefect_manager.wait_for_read_count_change(pre_read_data) - - post_read_data = praefect_manager.query_read_distribution - - aggregate_failures "each gitaly node" do - expect(praefect_manager.value_for_node(post_read_data, 'gitaly1')).to be > praefect_manager.value_for_node(pre_read_data, 'gitaly1') - expect(praefect_manager.value_for_node(post_read_data, 'gitaly2')).to eq praefect_manager.value_for_node(pre_read_data, 'gitaly2') - expect(praefect_manager.value_for_node(post_read_data, 'gitaly3')).to be > praefect_manager.value_for_node(pre_read_data, 'gitaly3') - end - end - end - - def read_from_project(project, number_of_reads) - QA::Runtime::Logger.info('Reading from the repository') - Parallel.each((1..number_of_reads)) do - Git::Repository.perform do |repository| - repository.uri = project.repository_http_location.uri - repository.use_default_credentials - repository.clone - end - end - end - - def wait_for_reads_to_increase(project, number_of_reads, pre_read_data) - diff_found = pre_read_data - - Support::Waiter.wait_until(sleep_interval: 5, raise_on_failure: false) do - read_from_project(project, number_of_reads) - - praefect_manager.query_read_distribution.each_with_index do |data, index| - diff_found[index] = {} unless diff_found[index] - diff_found[index][:diff] = true if data[:value] > praefect_manager.value_for_node(pre_read_data, data[:node]) - end - diff_found.all? { |node| node.key?(:diff) && node[:diff] } - end - end - end - end - end -end diff --git a/qa/qa/specs/features/api/3_create/gitaly/gitaly_mtls_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/gitaly_mtls_spec.rb deleted file mode 100644 index 5000c273578..00000000000 --- a/qa/qa/specs/features/api/3_create/gitaly/gitaly_mtls_spec.rb +++ /dev/null @@ -1,43 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Create' do - context 'Gitaly', :orchestrated, :mtls do - describe 'Using mTLS' do - let(:intial_commit_message) { 'Initial commit' } - let(:first_added_commit_message) { 'commit over git' } - let(:second_added_commit_message) { 'commit over api' } - - it 'pushes to gitaly', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347677' do - project = Resource::Project.fabricate! do |project| - project.name = "mTLS" - project.initialize_with_readme = true - end - - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.new_branch = false - push.commit_message = first_added_commit_message - push.file_content = 'First commit' - end - - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project - commit.commit_message = second_added_commit_message - commit.add_files([ - { - file_path: "file-#{SecureRandom.hex(8)}", - content: 'Second commit' - } - ]) - end - - expect(project.commits.map { |commit| commit[:message].chomp }) - .to include(intial_commit_message) - .and include(first_added_commit_message) - .and include(second_added_commit_message) - end - end - end - end -end diff --git a/qa/qa/specs/features/api/3_create/repository/commit_to_templated_project_spec.rb b/qa/qa/specs/features/api/3_create/repository/commit_to_templated_project_spec.rb index c06912e0367..9d47872a774 100644 --- a/qa/qa/specs/features/api/3_create/repository/commit_to_templated_project_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/commit_to_templated_project_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Create a new project from a template' do + describe 'Create a new project from a template', product_group: :source_code do let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'templated-project' diff --git a/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb b/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb index cba563ef85a..123a4a625ab 100644 --- a/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Default branch name instance setting', :requires_admin, :skip_live_env do + describe 'Default branch name instance setting', :requires_admin, :skip_live_env, product_group: :source_code do before(:context) do Runtime::ApplicationSettings.set_application_settings(default_branch_name: 'main') end diff --git a/qa/qa/specs/features/api/3_create/repository/files_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb index 151fd0fffe3..71bd03fab17 100644 --- a/qa/qa/specs/features/api/3_create/repository/files_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb @@ -4,7 +4,7 @@ require 'airborne' module QA RSpec.describe 'Create' do - describe 'API basics' do + describe 'API basics', product_group: :source_code do before(:context) do @api_client = Runtime::API::Client.new(:gitlab) end 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 6f175272d91..a211eb6042d 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 @@ -5,7 +5,8 @@ require 'digest' module QA RSpec.describe 'Create' do - describe 'Compare archives of different user projects with the same name and check they\'re different' do + describe 'Compare archives of different user projects with the same name and check they\'re different', + product_group: :source_code do include Support::API let(:project_name) { "project-archive-download-#{SecureRandom.hex(8)}" } @@ -52,12 +53,11 @@ module QA project.api_client = api_client end - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.file_name = 'README.md' - push.file_content = '# This is a test project' - push.commit_message = 'Add README.md' - push.user = user + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.add_files([{ file_path: 'README.md', content: '# This is a test project' }]) + commit.commit_message = 'Add README.md' + commit.api_client = api_client end project @@ -65,7 +65,7 @@ module QA def download_project_archive_via_api(api_client, project, type = 'tar.gz') get_project_archive_zip = Runtime::API::Request.new(api_client, project.api_get_archive_path(type)) - project_archive_download = get(get_project_archive_zip.url, raw_response: true) + project_archive_download = Support::API.get(get_project_archive_zip.url, raw_response: true) expect(project_archive_download.code).to eq(200) project_archive_download.file diff --git a/qa/qa/specs/features/api/3_create/repository/push_postreceive_idempotent_spec.rb b/qa/qa/specs/features/api/3_create/repository/push_postreceive_idempotent_spec.rb index 1a2a1679cca..1d41184df98 100644 --- a/qa/qa/specs/features/api/3_create/repository/push_postreceive_idempotent_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/push_postreceive_idempotent_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'PostReceive idempotent' do + describe 'PostReceive idempotent', product_group: :source_code do # Tests that a push does not result in multiple changes from repeated PostReceive executions. # One of the consequences would be duplicate push events diff --git a/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb b/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb index 406ff191f95..df3b5a4e7fb 100644 --- a/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Repository Usage Quota', :skip_live_env, feature_flag: { + describe 'Repository Usage Quota', :skip_live_env, product_group: :source_code, feature_flag: { name: 'gitaly_revlist_for_repo_size', scope: :global } do diff --git a/qa/qa/specs/features/api/3_create/repository/tag_revision_trigger_prereceive_hook_spec.rb b/qa/qa/specs/features/api/3_create/repository/tag_revision_trigger_prereceive_hook_spec.rb index 98612d84b21..b34b4208337 100644 --- a/qa/qa/specs/features/api/3_create/repository/tag_revision_trigger_prereceive_hook_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/tag_revision_trigger_prereceive_hook_spec.rb @@ -2,29 +2,32 @@ module QA RSpec.describe 'Create' do - let(:project) do - Resource::Project.fabricate_via_api! do |project| - project.initialize_with_readme = true + describe 'Prereceive hook', product_group: :source_code do + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.initialize_with_readme = true + end end - end - context 'when creating a tag for a ref' do - context 'when it triggers a prereceive hook configured with a custom error' do - before do - # The configuration test prereceive hook must match a specific naming pattern - # In this test we create a project with a different name and then change the path. - # Otherwise we wouldn't be able create any commits to be tagged due to the hook. - project.change_path("project-reject-prereceive-#{SecureRandom.hex(8)}") - end + context 'when creating a tag for a ref' do + context 'when it triggers a prereceive hook configured with a custom error' do + before do + # The configuration test prereceive hook must match a specific naming pattern + # In this test we create a project with a different name and then change the path. + # Otherwise we wouldn't be able create any commits to be tagged due to the hook. + project.change_path("project-reject-prereceive-#{SecureRandom.hex(8)}") + end - it 'returns a custom server hook error', - :skip_live_env, - except: { job: 'review-qa-*' }, - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/369053' do - expect { project.create_repository_tag('v1.2.3') }.to raise_error - .with_message( - /rejecting prereceive hook for projects with GL_PROJECT_PATH matching pattern reject-prereceive/ - ) + it 'returns a custom server hook error', + :skip_live_env, + except: { job: 'review-qa-*' }, + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/369053' do + expect { project.create_repository_tag('v1.2.3') } + .to raise_error + .with_message( + /rejecting prereceive hook for projects with GL_PROJECT_PATH matching pattern reject-prereceive/ + ) + end end end end diff --git a/qa/qa/specs/features/api/4_verify/file_variable_spec.rb b/qa/qa/specs/features/api/4_verify/file_variable_spec.rb new file mode 100644 index 00000000000..9722f62d5a7 --- /dev/null +++ b/qa/qa/specs/features/api/4_verify/file_variable_spec.rb @@ -0,0 +1,142 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Verify', :runner, feature_flag: { + name: 'ci_stop_expanding_file_vars_for_runners', + scope: :project + } do + describe 'Pipeline with project file variables' do + let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'project-with-file-variables' + end + end + + let(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + let(:add_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 + variables: + EXTRA_ARGS: "-f $TEST_FILE" + DOCKER_REMOTE_ARGS: --tlscacert="$DOCKER_CA_CERT" + EXTRACTED_CRT_FILE: ${DOCKER_CA_CERT}.crt + MY_FILE_VAR: $TEST_FILE + + test: + tags: [#{executor}] + script: + - echo "run something $EXTRA_ARGS" + - echo "docker run $DOCKER_REMOTE_ARGS" + - echo "run --output=$EXTRACTED_CRT_FILE" + - echo "Will read private key from $MY_FILE_VAR" + YAML + } + ] + ) + end + end + + let(:add_file_variables) do + { + 'TEST_FILE' => 'hello, this is test', + 'DOCKER_CA_CERT' => 'This is secret' + }.each do |file_name, content| + add_file_variable_to_project(file_name, content) + end + end + + after do + runner.remove_via_api! + end + + shared_examples 'variables are read correctly' do + it 'shows in job log accordingly' do + job = Resource::Job.fabricate_via_api! do |job| + job.project = project + job.id = project.job_by_name('test')[:id] + end + + aggregate_failures do + trace = job.trace + expect(trace).to have_content('run something -f hello, this is test') + expect(trace).to have_content('docker run --tlscacert="This is secret"') + expect(trace).to have_content('run --output=This is secret.crt') + expect(trace).to have_content('Will read private key from hello, this is test') + end + end + end + + # FF does not change current behavior + # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/94198#note_1057609893 + # + # TODO: Remove when FF is removed + # TODO: Archive testcase issue when FF is removed + # Rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/369907 + context 'when FF is on', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370787' do + before do + Runtime::Feature.enable(:ci_stop_expanding_file_vars_for_runners, project: project) + + runner + add_file_variables + add_ci_file + trigger_pipeline + wait_for_pipeline + end + + it_behaves_like 'variables are read correctly' + end + + # TODO: Refactor when FF is removed + # TODO: Update testcase issue title and description to not refer to FF status + context 'when FF is off', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370791' do + before do + runner + add_file_variables + add_ci_file + trigger_pipeline + wait_for_pipeline + end + + it_behaves_like 'variables are read correctly' + end + + private + + def add_file_variable_to_project(key, value) + Resource::CiVariable.fabricate_via_api! do |ci_variable| + ci_variable.project = project + ci_variable.key = key + ci_variable.value = value + ci_variable.variable_type = 'file' + end + end + + def trigger_pipeline + Resource::Pipeline.fabricate_via_api! do |pipeline| + pipeline.project = project + end + end + + def wait_for_pipeline + Support::Waiter.wait_until do + project.pipelines.present? && project.pipelines.first[:status] == 'success' + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb index c5efa833f04..ad90df4b90d 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb @@ -36,7 +36,11 @@ module QA group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER) end - it 'allows enforcing 2FA via UI and logging in with 2FA', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347931' do + it( + 'allows enforcing 2FA via UI and logging in with 2FA', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347931', + quarantine: { type: :flaky, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/369516' } + ) do enforce_two_factor_authentication_on_group(group) enable_two_factor_authentication_for_user(developer_user) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 8cc772ed022..295702aa328 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -66,7 +66,7 @@ module QA end # TODO: Remove retry_on_exception once https://gitlab.com/gitlab-org/gitlab/-/issues/24294 is resolved - Support::Waiter.wait_until(retry_on_exception: true, sleep_interval: 3) { !user.exists? } + Support::Waiter.wait_until(max_duration: 120, retry_on_exception: true, sleep_interval: 3) { !user.exists? } end it 'allows recreating with same credentials', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347868' do diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb index b1d59b90e9c..d299997dd3c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb @@ -20,8 +20,10 @@ module QA Flow::Login.sign_in(as: user) Page::Dashboard::Welcome.perform do |welcome| - expect(welcome).to have_welcome_title("Welcome to GitLab") - + Support::Waiter.wait_until(sleep_interval: 2, max_duration: 60, reload_page: page, + retry_on_exception: true) do + expect(welcome).to have_welcome_title("Welcome to GitLab") + end # This would be better if it were a visual validation test expect(welcome).to have_loaded_all_images end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index ed271533f87..7e4a391c390 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -64,7 +64,7 @@ module QA it( 'comments on an issue with an attachment', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347946', - except: { job: 'review-qa-smoke' } + except: { job: 'review-qa-*' } ) do Page::Project::Issue::Show.perform do |show| show.comment('See attached image for scale', attachment: file_to_attach) diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index d198d79c5fe..109cf7b73c3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -17,7 +17,11 @@ module QA merge_request.fork.remove_via_api! end - it 'can merge feature branch fork to mainline', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818' do + it 'can merge feature branch fork to mainline', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347818', quarantine: { + only: :production, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/372258' + } do merge_request.visit! Page::MergeRequest::Show.perform do |merge_request| diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index 2280cc971a7..c7296b6eea2 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -12,11 +12,9 @@ module QA it 'user rebases source branch of merge request', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347735' do merge_request.project.visit! - Page::Project::Menu.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform do |main| - main.expand_merge_requests_settings do |settings| - settings.enable_ff_only - end + Page::Project::Menu.perform(&:go_to_merge_request_settings) + Page::Project::Settings::MergeRequest.perform do |settings| + settings.enable_ff_only end Resource::Repository::ProjectPush.fabricate! do |push| diff --git a/qa/qa/specs/features/browser_ui/3_create/pages/pages_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/3_create/pages/new_static_page_spec.rb index 191c4a096e7..9c912080c5f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/pages/pages_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/pages/new_static_page_spec.rb @@ -1,7 +1,6 @@ # frozen_string_literal: true - module QA - RSpec.describe 'Release', :runner do + RSpec.describe 'Create', :runner, only: { subdomain: :staging } do # TODO: Convert back to :smoke once proved to be stable. Related issue: https://gitlab.com/gitlab-org/gitlab/-/issues/300906 describe 'Pages' do let!(:project) do @@ -27,12 +26,11 @@ module QA runner.project = project runner.executor = :docker end - pipeline.visit! end - it('runs a Pages-specific pipeline', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347669') do + it 'creates a Pages website', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347669' do Page::Project::Pipeline::Show.perform do |show| expect(show).to have_job(:pages) show.click_job(:pages) @@ -41,6 +39,15 @@ module QA Page::Project::Job::Show.perform do |show| expect(show).to have_passed(timeout: 300) end + + Page::Project::Show.perform(&:go_to_pages_settings) + QA::Page::Project::Settings::Pages.perform do |pages| + pages.go_to_access_page + Support::Waiter.wait_until(sleep_interval: 2, max_duration: 60, reload_page: page, + retry_on_exception: true) do + expect(page).to have_content('Write an awesome description for your new site here.') + end + end end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index 107d72a9724..b4103bd0976 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'File templates' do + describe 'File templates', product_group: :source_code do include Runtime::Fixtures let(:project) do diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb index 0bd470fcb77..849022f5a93 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Create, list, and delete branches via web', :requires_admin do + describe 'Create, list, and delete branches via web', :requires_admin, product_group: :source_code do master_branch = nil second_branch = 'second-branch' third_branch = 'third-branch' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb index 9e77fd228da..aa332a76c94 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/branch_with_unusual_name_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Branch with unusual name' do + describe 'Branch with unusual name', product_group: :source_code do let(:branch_name) { 'unUsually/named#br--anch' } let(:project) do Resource::Project.fabricate_via_api! do |resource| diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index d12fb05af77..b7df22fc2c2 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Git clone over HTTP' do + describe 'Git clone over HTTP', product_group: :source_code do let(:project) do Resource::Project.fabricate_via_api! do |scenario| scenario.name = 'project-with-code' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb index 095444d99f1..d5f30c19738 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/create_file_via_web_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', :reliable do - context 'File management' do + RSpec.describe 'Create', :reliable, product_group: :source_code do + describe 'File management' do file_name = 'QA Test - File name' file_content = 'QA Test - File content' commit_message_for_create = 'QA Test - Create new file' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb index 02ecff22840..76243066476 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/delete_file_via_web_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - context 'File management' do + describe 'File management', product_group: :source_code do let(:file) { Resource::File.fabricate_via_api! } commit_message_for_delete = 'QA Test - Delete file' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb index 95e7a2a12d0..397796b76e5 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/edit_file_via_web_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create', :reliable do - context 'File management' do + describe 'File management', product_group: :source_code do let(:file) { Resource::File.fabricate_via_api! } updated_file_content = 'QA Test - Updated file content' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/file/file_with_unusual_name_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/file/file_with_unusual_name_spec.rb index eb6449181b5..d7da29219e6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/file/file_with_unusual_name_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/file/file_with_unusual_name_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'File with unusual name' do + describe 'File with unusual name', product_group: :source_code do let(:file_name) { '-un:usually;named#file?.md' } let(:project) do Resource::Project.fabricate_via_api! do |resource| diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/license_detecton_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/license_detecton_spec.rb index 3db8128bc6d..1ae1dd87c07 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/license_detecton_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/license_detecton_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Repository License Detection' do + describe 'Repository License Detection', product_group: :source_code do after do project.remove_via_api! end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb index 153bfd292aa..a391e6313a6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', :orchestrated, :repository_storage, :requires_admin do + RSpec.describe 'Create', :orchestrated, :repository_storage, :requires_admin, product_group: :source_code do describe 'Gitaly repository storage' do let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } let(:parent_project) do diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protected_tags_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protected_tags_spec.rb index fb87ca864f4..65a15ce96a5 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protected_tags_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protected_tags_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Repository tags', :reliable do + describe 'Repository tags', :reliable, product_group: :source_code do let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'project-for-tags' diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb index 34439042796..557a27c002d 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2 do + describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2, product_group: :source_code do it 'user pushes to the repository', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347760' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb index 25d4da95dd9..2472d1cdf25 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Push over SSH using Git protocol version 2', :requires_git_protocol_v2 do + describe 'Push over SSH using Git protocol version 2', :requires_git_protocol_v2, product_group: :source_code do # Note: If you run this test against GDK make sure you've enabled sshd and # enabled setting the Git protocol by adding `AcceptEnv GIT_PROTOCOL` to # `sshd_config` diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index 042fee38188..a785e4ae764 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Git push over HTTP', :smoke do + describe 'Git push over HTTP', :smoke, product_group: :source_code do it 'user using a personal access token pushes code to the repository', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347749' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb index 9ab322df824..815a8696ff7 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', quarantine: { + RSpec.describe 'Create', product_group: :source_code, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/352525', type: :test_environment, only: { job: 'review-qa-*' } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb index d644a7ead1e..b87c47761bd 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Push mirror a repository over HTTP' do + describe 'Push mirror a repository over HTTP', product_group: :source_code do it 'configures and syncs a (push) mirrored repository', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347741' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb index 0e4aa67162f..324dbbc46ef 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb @@ -3,7 +3,7 @@ module QA # This test modifies an instance level setting, # so skipping on live envs to avoid random transient issues - RSpec.describe 'Create', :requires_admin, :skip_live_env do + RSpec.describe 'Create', :requires_admin, :skip_live_env, product_group: :source_code do describe 'push after setting the file size limit via admin/application_settings' do include Support::API diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index a782a50b55d..e8f7cb252a0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Git push over HTTP' do + describe 'Git push over HTTP', product_group: :source_code do it 'user pushes code to the repository', :smoke, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347747' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb index 0323448878b..4fb52f1e54d 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'SSH key support' do + describe 'SSH key support', product_group: :source_code do # Note: If you run these tests against GDK make sure you've enabled sshd # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index 91b0940f137..e097e1fd2bb 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', :reliable do + RSpec.describe 'Create', :reliable, product_group: :source_code do describe 'Protected branch support' do let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_to_canary_gitaly_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_to_canary_gitaly_spec.rb index 78abdb94dfe..d95a880c305 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_to_canary_gitaly_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_to_canary_gitaly_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', only: { subdomain: %i[staging staging-canary] } do + RSpec.describe 'Create', only: { subdomain: %i[staging staging-canary] }, product_group: :source_code do describe 'Git push to canary Gitaly node over HTTP' do it 'pushes to a project using a canary specific Gitaly repository storage', :smoke, :requires_admin, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/351116' do Flow::Login.sign_in_as_admin diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb index 55df1615f5c..64858287285 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'SSH keys support', :smoke do + describe 'SSH keys support', :smoke, product_group: :source_code do let(:key_title) { "key for ssh tests #{Time.now.to_f}" } key = nil diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb index bbf6c3ca37a..13b42209114 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/user_views_commit_diff_patch_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Commit data' do + describe 'Commit data', product_group: :source_code do before(:context) do # Get the user's details to confirm they're included in the email patch @user = Resource::User.fabricate_via_api! do |user| diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb new file mode 100644 index 00000000000..8352ad6aa33 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/prefill_variables_spec.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Verify' do + describe 'Pipeline with prefill variables' do + let(:prefill_variable_description1) { Faker::Lorem.sentence } + let(:prefill_variable_value1) { Faker::Lorem.word } + let(:prefill_variable_description2) { Faker::Lorem.sentence } + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'project-with-prefill-variables' + end + end + + let!(:commit) 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 + variables: + TEST1: + value: #{prefill_variable_value1} + description: #{prefill_variable_description1} + TEST2: + description: #{prefill_variable_description2} + TEST3: + value: test 3 value + TEST4: test 4 value + + test: + script: echo "$FOO" + YAML + } + ] + ) + end + end + + before do + Flow::Login.sign_in + project.visit! + + # Navigate to Run Pipeline page + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) + end + + it( + 'shows only variables with description as prefill variables on the run pipeline page', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/371204' + ) do + Page::Project::Pipeline::New.perform do |new| + aggregate_failures do + expect(new).to have_field('Input variable key', with: 'TEST1') + expect(new).to have_field('Input variable value', with: prefill_variable_value1) + expect(new).to have_content(prefill_variable_description1) + + expect(new).to have_field('Input variable key', with: 'TEST2') + expect(new).to have_content(prefill_variable_description2) + + expect(new).not_to have_field('Input variable key', with: 'TEST3') + expect(new).not_to have_field('Input variable key', with: 'TEST4') + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb index 412498476f0..f9113573295 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb @@ -1,12 +1,7 @@ # frozen_string_literal: true module QA - # TODO: remove feature flag upon rollout completion - # FF rollout issue: https://gitlab.com/gitlab-org/gitlab/-/issues/363186 - RSpec.describe 'Verify', :runner, feature_flag: { - name: 'ci_docker_image_pull_policy', - scope: :global - } do + RSpec.describe 'Verify', :runner do describe 'Pipeline with image:pull_policy' do let(:runner_name) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(number: 8)}" } let(:job_name) { "test-job-#{pull_policies.join('-')}" } @@ -27,10 +22,6 @@ module QA end before do - Runtime::Feature.enable(:ci_docker_image_pull_policy) - # Give the feature some time to switch - sleep(30) - update_runner_policy(allowed_policies) add_ci_file Flow::Login.sign_in @@ -39,12 +30,13 @@ module QA end after do - Runtime::Feature.disable(:ci_docker_image_pull_policy) - runner.remove_via_api! end - context 'when policy is allowed' do + context( + 'when policy is allowed', + quarantine: { type: :flaky, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/369397' } + ) do let(:allowed_policies) { %w[if-not-present always never] } where do @@ -102,7 +94,8 @@ module QA it( 'fails job with policy not allowed message', - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368853' + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368853', + quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/371420', type: :stale } ) do visit_job diff --git a/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb new file mode 100644 index 00000000000..6ce395affc7 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/5_package/infrastructure_registry/terraform_module_registry_spec.rb @@ -0,0 +1,81 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Package', :orchestrated, :packages do + describe 'Terraform Module Registry' do + include Runtime::Fixtures + + let(:group) { Resource::Group.fabricate_via_api! } + + let(:imported_project) do + Resource::ProjectImportedFromURL.fabricate_via_browser_ui! do |project| + project.name = 'terraform-module-test' + project.group = group + project.gitlab_repository_path = 'https://gitlab.com/mattkasa/terraform-module-test.git' + end + end + + let(:runner) do + Resource::Runner.fabricate! do |runner| + runner.name = "qa-runner-#{Time.now.to_i}" + runner.tags = ["runner-for-#{imported_project.name}"] + runner.executor = :docker + runner.project = imported_project + runner.token = group.reload!.runners_token + end + end + + before do + Flow::Login.sign_in + + imported_project + runner + end + + after do + runner.remove_via_api! + end + + it 'publishes a module', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/371583' do + Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + terraform_module_yaml = ERB.new( + read_fixture('package_managers/terraform', 'module_upload.yaml.erb') + ).result(binding) + commit.project = imported_project + commit.commit_message = 'Add gitlab-ci.yaml file' + commit.update_files([ + { + file_path: '.gitlab-ci.yml', + content: terraform_module_yaml + } + ] + ) + end + end + + Resource::Tag.fabricate_via_api! do |tag| + tag.project = imported_project + tag.ref = imported_project.default_branch + tag.name = "1.0.0" + end + + Flow::Pipeline.visit_latest_pipeline + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('upload') + end + + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 800) + end + + Page::Project::Menu.perform(&:go_to_infrastructure_registry) + + Page::Project::Packages::Index.perform do |index| + expect(index).to have_module("#{imported_project.name}/local") + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb index 180910e85a0..921b36b34af 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb @@ -142,7 +142,7 @@ module QA Page::Group::Menu.perform(&:go_to_package_settings) end - context 'when enabled' do + context 'when disabled' do where do { 'using a personal access token' => { @@ -176,7 +176,7 @@ module QA end before do - Page::Group::Settings::PackageRegistries.perform(&:set_reject_duplicates_enabled) + Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled) end it 'prevents users from publishing group level Maven packages duplicates', testcase: params[:testcase] do @@ -195,7 +195,7 @@ module QA end end - context 'when disabled' do + context 'when enabled' do where do { 'using a personal access token' => { @@ -229,7 +229,7 @@ module QA end before do - Page::Group::Settings::PackageRegistries.perform(&:set_reject_duplicates_disabled) + Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled) end it 'allows users to publish group level Maven packages duplicates', testcase: params[:testcase] do diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb index 22d76d684e5..012a03ca115 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb @@ -105,10 +105,6 @@ module QA QA::Runtime::Logger.debug('Visiting the secondary Geo site') QA::Flow::Login.while_signed_in(address: :geo_secondary) do - EE::Page::Main::Banner.perform do |banner| - expect(banner).to have_secondary_read_only_banner - end - Page::Main::Menu.perform(&:go_to_projects) Page::Dashboard::Projects.perform do |dashboard| diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index f1a2eb71390..b839855c500 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -1,78 +1,67 @@ # frozen_string_literal: true module QA - RSpec.describe 'Configure', - only: { subdomain: %i[staging staging-canary] }, - quarantine: { - issue: 'https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1198', - type: :waiting_on - } do - let(:project) do - Resource::Project.fabricate_via_api! do |project| - project.name = 'autodevops-project' - project.auto_devops_enabled = true + RSpec.describe 'Configure', only: { subdomain: %i[staging staging-canary] } do + describe 'Auto DevOps with a Kubernetes Agent' do + let!(:app_project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'autodevops-app-project' + project.template_name = 'express' + project.auto_devops_enabled = true + end end - end - before do - set_kube_ingress_base_domain(project) - disable_optional_jobs(project) - end + let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::Gcloud).create! } - describe 'Auto DevOps support' do - context 'when rbac is enabled' do - let(:cluster) { Service::KubernetesCluster.new.create! } + let!(:kubernetes_agent) do + Resource::Clusters::Agent.fabricate_via_api! do |agent| + agent.name = 'agent1' + agent.project = app_project + end + end - after do - cluster&.remove! - project.remove_via_api! + let!(:agent_token) do + Resource::Clusters::AgentToken.fabricate_via_api! do |token| + token.agent = kubernetes_agent end + end - it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do - Flow::Login.sign_in - - Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster| - k8s_cluster.project = project - k8s_cluster.cluster = cluster - k8s_cluster.install_ingress = true - end - - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.directory = Pathname - .new(__dir__) - .join('../../../../../fixtures/auto_devops_rack') - push.commit_message = 'Create Auto DevOps compatible rack application' - end - - Flow::Pipeline.visit_latest_pipeline - - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('build') - end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 600) - - job.click_element(:pipeline_path) - end - - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('test') - end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 600) - - job.click_element(:pipeline_path) - end - - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('production') - end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 1200) - - job.click_element(:pipeline_path) - end + before do + cluster.install_kubernetes_agent(agent_token.token) + upload_agent_config(app_project, kubernetes_agent.name) + + set_kube_ingress_base_domain(app_project) + set_kube_context(app_project) + disable_optional_jobs(app_project) + end + + after do + cluster&.remove! + end + + it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348061' do + Flow::Login.sign_in + + app_project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) + Page::Project::Pipeline::New.perform(&:click_run_pipeline_button) + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('build') + end + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 600) + + job.click_element(:pipeline_path) + end + + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('production') + end + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 600) end end end @@ -88,12 +77,43 @@ module QA end end + def set_kube_context(project) + Resource::CiVariable.fabricate_via_api! do |resource| + resource.project = project + resource.key = 'KUBE_CONTEXT' + resource.value = "#{project.path_with_namespace}:#{kubernetes_agent.name}" + resource.masked = false + end + end + + def upload_agent_config(project, agent) + Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add kubernetes agent configuration' + commit.add_files( + [ + { + file_path: ".gitlab/agents/#{agent}/config.yaml", + content: <<~YAML + ci_access: + projects: + - id: #{project.path_with_namespace} + YAML + } + ] + ) + end + end + end + def disable_optional_jobs(project) %w[ - CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED - SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED - CONTAINER_SCANNING_DISABLED BROWSER_PERFORMANCE_DISABLED - SECRET_DETECTION_DISABLED + TEST_DISABLED CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED + BROWSER_PERFORMANCE_DISABLED LOAD_PERFORMANCE_DISABLED + SAST_DISABLED SECRET_DETECTION_DISABLED DEPENDENCY_SCANNING_DISABLED + CONTAINER_SCANNING_DISABLED DAST_DISABLED REVIEW_DISABLED + CODE_INTELLIGENCE_DISABLED CLUSTER_IMAGE_SCANNING_DISABLED ].each do |key| Resource::CiVariable.fabricate_via_api! do |resource| resource.project = project 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 deleted file mode 100644 index 94f9e9ec1f6..00000000000 --- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Configure', except: { job: 'review-qa-*' } do - describe 'Kubernetes Cluster Integration', :orchestrated, :requires_admin, :skip_live_env do - context 'Project Clusters' do - let!(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! } - let(:project) do - Resource::Project.fabricate_via_api! do |project| - project.name = 'project-with-k8s' - project.description = 'Project with Kubernetes cluster integration' - end - end - - before do - Flow::Login.sign_in_as_admin - end - - after do - cluster.remove! - end - - it 'can create and associate a project cluster', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348062' do - Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster| - k8s_cluster.project = project - k8s_cluster.cluster = cluster - end.project.visit! - - Page::Project::Menu.perform(&:go_to_infrastructure_kubernetes) - - Page::Project::Infrastructure::Kubernetes::Index.perform do |index| - expect(index).to have_cluster(cluster) - end - end - end - end - end -end diff --git a/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb b/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb index 4bbad9bf3e5..01b229192cc 100644 --- a/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb +++ b/qa/qa/specs/features/shared_examples/merge_with_code_owner_shared_examples.rb @@ -8,11 +8,9 @@ module QA # Require one approval from any eligible user on any branch # This will confirm that this type of unrestricted approval is # also satisfied when a code owner grants approval - Page::Project::Menu.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform do |main| - main.expand_merge_request_approvals_settings do |settings| - settings.set_default_number_of_approvals_required(1) - end + Page::Project::Menu.perform(&:go_to_merge_request_settings) + Page::Project::Settings::MergeRequest.perform do |settings| + settings.set_default_number_of_approvals_required(1) end Resource::Repository::Commit.fabricate_via_api! do |commit| diff --git a/qa/qa/specs/helpers/context_selector.rb b/qa/qa/specs/helpers/context_selector.rb index 9ac79ad6196..3608fa7c581 100644 --- a/qa/qa/specs/helpers/context_selector.rb +++ b/qa/qa/specs/helpers/context_selector.rb @@ -30,6 +30,8 @@ module QA next unless option.is_a?(Hash) + opts.merge!(option) + if option[:pipeline].present? return true if Runtime::Env.ci_project_name.blank? @@ -41,8 +43,6 @@ module QA return job_matches?(option[:job]) elsif option[:subdomain].present? - opts.merge!(option) - opts[:subdomain] = case option[:subdomain] when Array "(#{option[:subdomain].join("|")})\\." diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index 801b9b222a4..c46b6300200 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -86,8 +86,7 @@ module QA File.open(filename, 'w') { |f| f.write(total_examples) } if total_examples.to_i > 0 - saved_file_msg = total_examples.to_i > 0 ? ". Saved to file: #{filename}" : '' - $stdout.puts "Total examples in #{Runtime::Scenario.klass}: #{total_examples}#{saved_file_msg}" + $stdout.puts total_examples end def test_metadata_only(args) diff --git a/qa/qa/specs/spec_helper.rb b/qa/qa/specs/spec_helper.rb index e1c08515521..b9e67c2fa72 100644 --- a/qa/qa/specs/spec_helper.rb +++ b/qa/qa/specs/spec_helper.rb @@ -12,6 +12,9 @@ QA::Runtime::Browser.configure! unless QA::Runtime::Env.dry_run QA::Runtime::AllureReport.configure! QA::Runtime::Scenario.from_env(QA::Runtime::Env.runtime_scenario_attributes) +# Enable zero monkey patching mode before loading any other RSpec code. +RSpec.configure(&:disable_monkey_patching!) + Dir[::File.join(__dir__, "features/shared_examples/*.rb")].sort.each { |f| require f } Dir[::File.join(__dir__, "features/shared_contexts/*.rb")].sort.each { |f| require f } @@ -119,7 +122,6 @@ RSpec.configure do |config| end config.shared_context_metadata_behavior = :apply_to_host_groups - config.disable_monkey_patching! config.expose_dsl_globally = true config.profile_examples = 10 config.order = :random diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index a1bbe9f378a..8fa6d3f23dc 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -3,10 +3,13 @@ module QA module Support module API + extend self + HTTP_STATUS_OK = 200 HTTP_STATUS_CREATED = 201 HTTP_STATUS_NO_CONTENT = 204 HTTP_STATUS_ACCEPTED = 202 + HTTP_STATUS_PERMANENT_REDIRECT = 308 HTTP_STATUS_NOT_FOUND = 404 HTTP_STATUS_TOO_MANY_REQUESTS = 429 HTTP_STATUS_SERVER_ERROR = 500 @@ -21,7 +24,7 @@ module QA } RestClient::Request.execute(default_args.merge(args)) - rescue RestClient::ExceptionWithResponse => e + rescue StandardError => e return_response_or_raise(e) end end @@ -37,7 +40,7 @@ module QA RestClient::Request.execute( default_args.merge(args) ) - rescue RestClient::ExceptionWithResponse => e + rescue StandardError => e return_response_or_raise(e) end end @@ -49,7 +52,7 @@ module QA url: url, payload: payload, verify_ssl: false) - rescue RestClient::ExceptionWithResponse => e + rescue StandardError => e return_response_or_raise(e) end end @@ -64,7 +67,7 @@ module QA } RestClient::Request.execute(default_args.merge(args)) - rescue RestClient::ExceptionWithResponse => e + rescue StandardError => e return_response_or_raise(e) end end @@ -75,7 +78,7 @@ module QA method: :delete, url: url, verify_ssl: false) - rescue RestClient::ExceptionWithResponse => e + rescue StandardError => e return_response_or_raise(e) end end @@ -86,11 +89,15 @@ module QA method: :head, url: url, verify_ssl: false) - rescue RestClient::ExceptionWithResponse => e + rescue StandardError => e return_response_or_raise(e) end end + def masked_url(url) + url.sub(/private_token=[^&]*/, "private_token=[****]") + end + def with_retry_on_too_many_requests response = nil @@ -115,7 +122,7 @@ module QA end def return_response_or_raise(error) - raise error unless error.respond_to?(:response) && error.response + raise error, masked_url(error.to_s) unless error.respond_to?(:response) && error.response error.response end @@ -129,7 +136,7 @@ module QA 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}`." + raise "Failed to GET #{masked_url(url)} - (#{resp.code}): `#{resp}`." end loop do diff --git a/qa/qa/support/json_formatter.rb b/qa/qa/support/json_formatter.rb index 0b805cd9eec..252ccfe73d3 100644 --- a/qa/qa/support/json_formatter.rb +++ b/qa/qa/support/json_formatter.rb @@ -51,6 +51,7 @@ module QA testcase: example.metadata[:testcase], quarantine: example.metadata[:quarantine], screenshot: example.metadata[:screenshot], + product_group: example.metadata[:product_group], ci_job_url: QA::Runtime::Env.ci_job_url } end diff --git a/qa/qa/support/matchers/eventually_matcher.rb b/qa/qa/support/matchers/eventually_matcher.rb index 01d07585f57..3f451f89246 100644 --- a/qa/qa/support/matchers/eventually_matcher.rb +++ b/qa/qa/support/matchers/eventually_matcher.rb @@ -21,6 +21,7 @@ module QA eq be include + match be_truthy be_falsey be_empty diff --git a/qa/qa/tools/ci/ff_changes.rb b/qa/qa/tools/ci/ff_changes.rb new file mode 100644 index 00000000000..67e52633833 --- /dev/null +++ b/qa/qa/tools/ci/ff_changes.rb @@ -0,0 +1,66 @@ +# frozen_string_literal: true + +require "yaml" + +module QA + module Tools + module Ci + class FfChanges + include Helpers + + def initialize(mr_diff) + @mr_diff = mr_diff + end + + # Return list of feature flags changed in mr with inverse or deleted state + # + # @return [String] + def fetch + logger.info("Detecting feature flag changes") + ff_toggles = mr_diff.map do |change| + ff_yaml = ff_yaml_for_file(change) + next unless ff_yaml + + state = if ff_yaml[:deleted] + "deleted" + else + ff_yaml[:default_enabled] ? 'disabled' : 'enabled' + end + + logger.info(" found changes in feature flag '#{ff_yaml[:name]}'") + "#{ff_yaml[:name]}=#{state}" + end.compact + + if ff_toggles.empty? + logger.info(" no changes to feature flags detected, skipping!") + return + end + + logger.info(" constructed feature flag states: '#{ff_toggles}'") + ff_toggles.join(",") + end + + private + + attr_reader :mr_diff + + # Loads the YAML feature flag definition based on changed files in merge requests. + # The definition is loaded from the definition file itself. + # + # @param [Hash] change mr file change + # @return [Hash] a hash containing the YAML data for the feature flag definition + def ff_yaml_for_file(change) + return unless change[:path] =~ %r{/feature_flags/(development|ops)/.*\.yml} + if change[:deleted_file] + return { name: change[:path].split("/").last.gsub(/\.(yml|yaml)/, ""), deleted: true } + end + + YAML.safe_load( + File.read(File.expand_path("../#{change[:path]}", QA::Runtime::Path.qa_root)), + symbolize_names: true + ) + end + end + end + end +end diff --git a/qa/qa/tools/ci/helpers.rb b/qa/qa/tools/ci/helpers.rb new file mode 100644 index 00000000000..55bb123de20 --- /dev/null +++ b/qa/qa/tools/ci/helpers.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module QA + module Tools + module Ci + # Helpers for CI related tasks + # + module Helpers + include Support::API + + # Logger instance + # + # @return [Logger] + def logger + @logger ||= Gitlab::QA::TestLogger.logger( + level: Gitlab::QA::Runtime::Env.log_level, + source: "CI Tools" + ) + end + + # Api get request + # + # @param [String] path + # @param [Hash] args + # @return [Hash, Array] + def api_get(path, **args) + response = get("#{api_url}/#{path}", { headers: { "PRIVATE-TOKEN" => access_token }, **args }) + response = response.follow_redirection if response.code == Support::API::HTTP_STATUS_PERMANENT_REDIRECT + raise "Request failed: '#{response.body}'" unless response.code == Support::API::HTTP_STATUS_OK + + args[:raw_response] ? response : parse_body(response) + end + + # Gitlab api url + # + # @return [String] + def api_url + @api_url ||= ENV.fetch('CI_API_V4_URL', 'https://gitlab.com/api/v4') + end + + # Api access token + # + # @return [String] + def access_token + @access_token ||= ENV.fetch('QA_GITLAB_CI_TOKEN') { raise('Variable QA_GITLAB_CI_TOKEN missing') } + end + end + end + end +end diff --git a/qa/qa/tools/ci/non_empty_suites.rb b/qa/qa/tools/ci/non_empty_suites.rb new file mode 100644 index 00000000000..687c11a3e62 --- /dev/null +++ b/qa/qa/tools/ci/non_empty_suites.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +require 'open3' + +module QA + module Tools + module Ci + # Run count commands for scenarios and detect which ones have more than 0 examples to run + # + class NonEmptySuites + include Helpers + + # rubocop:disable Layout/LineLength + SCENARIOS = [ + { klass: "Test::Instance::All" }, + { klass: "Test::Instance::Smoke" }, + { klass: "Test::Instance::Reliable" }, + { klass: "Test::Instance::ReviewBlocking" }, + { klass: "Test::Instance::ReviewNonBlocking" }, + { klass: "Test::Instance::CloudActivation" }, + { klass: "Test::Instance::Integrations" }, + { klass: "Test::Instance::Jira" }, + { klass: "Test::Instance::LargeSetup" }, + { klass: "Test::Instance::Metrics" }, + { klass: "Test::Instance::ObjectStorage" }, + { klass: "Test::Instance::Packages" }, + { klass: "Test::Instance::RepositoryStorage" }, + { klass: "Test::Integration::ServicePingDisabled" }, + { klass: "Test::Integration::LDAPNoTLS" }, + { klass: "Test::Integration::LDAPTLS" }, + { klass: "Test::Integration::LDAPNoServer" }, + { klass: "Test::Integration::InstanceSAML" }, + { klass: "Test::Integration::RegistryWithCDN" }, + { klass: "Test::Integration::RegistryTLS" }, + { klass: "Test::Integration::Registry" }, + { klass: "Test::Integration::SMTP" }, + { klass: "QA::EE::Scenario::Test::Integration::Elasticsearch" }, + { klass: "QA::EE::Scenario::Test::Integration::GroupSAML" }, + { + klass: "QA::EE::Scenario::Test::Geo", + args: "--primary-address http://dummy1.test --primary-name gitlab-primary --secondary-address http://dummy2.test --secondary-name gitlab-secondary --without-setup" + }, + { + klass: "Test::Integration::Mattermost", + args: "--mattermost-address http://mattermost.test" + } + ].freeze + # rubocop:enable Layout/LineLength + + def initialize(qa_tests) + @qa_tests = qa_tests + end + + # Run counts and return runnable scenario list + # + # @return [String] + def fetch + logger.info("Checking for runnable suites") + scenarios = SCENARIOS.each_with_object([]) do |scenario, runnable_scenarios| + logger.info(" fetching runnable specs for '#{scenario[:klass]}'") + + out, err, status = run_command(**scenario) + + unless status.success? + logger.error(" example count failed!\n#{err}") + next + end + + count = out.split("\n").last.to_i + logger.info(" found #{count} examples to run") + runnable_scenarios << scenario[:klass] if count > 0 + end + + scenarios.join(",") + end + + private + + attr_reader :qa_tests + + # Run scenario count command + # + # @param [String] klass + # @param [String] args + # @return [String] + def run_command(klass:, args: nil) + cmd = ["bundle exec bin/qa"] + cmd << klass + cmd << "--count-examples-only --address http://dummy1.test" + cmd << args if args + cmd << "-- #{qa_tests}" unless qa_tests.blank? + + Open3.capture3(cmd.join(" ")) + end + end + end + end +end diff --git a/qa/qa/tools/ci/qa_changes.rb b/qa/qa/tools/ci/qa_changes.rb new file mode 100644 index 00000000000..75274961efe --- /dev/null +++ b/qa/qa/tools/ci/qa_changes.rb @@ -0,0 +1,116 @@ +# frozen_string_literal: true + +require "pathname" + +module QA + module Tools + module Ci + # Determine specific qa specs or paths to execute based on changes + class QaChanges + include Helpers + + QA_PATTERN = %r{^qa/}.freeze + SPEC_PATTERN = %r{^qa/qa/specs/features/}.freeze + + def initialize(mr_diff, mr_labels) + @mr_diff = mr_diff + @mr_labels = mr_labels + end + + # Specific specs to run + # + # @return [String] + def qa_tests + return if mr_diff.empty? + # make paths relative to qa directory + return changed_files&.map { |path| path.delete_prefix("qa/") }&.join(" ") if only_spec_changes? + return qa_spec_directories_for_devops_stage&.join(" ") if non_qa_changes? && mr_labels.any? + end + + # Qa framework changes + # + # @return [Boolean] + def framework_changes? + return false if mr_diff.empty? + return false if only_spec_changes? + + changed_files + # TODO: expand pattern to other non spec paths that shouldn't trigger full suite + .select { |file_path| file_path.match?(QA_PATTERN) && !file_path.match?(SPEC_PATTERN) } + .any? + end + + def quarantine_changes? + return false if mr_diff.empty? + return false if mr_diff.any? { |change| change[:new_file] || change[:deleted_file] } + + files_count = 0 + specs_count = 0 + quarantine_specs_count = 0 + + mr_diff.each do |change| + path = change[:path] + next if File.directory?(File.expand_path("../#{path}", QA::Runtime::Path.qa_root)) + + files_count += 1 + next unless path.match?(SPEC_PATTERN) && path.end_with?('_spec.rb') + + specs_count += 1 + quarantine_specs_count += 1 if change[:diff].match?(/^\+.*,? quarantine:/) + end + + return false if specs_count == 0 + return true if quarantine_specs_count == specs_count && quarantine_specs_count == files_count + + false + end + + private + + # @return [Array] + attr_reader :mr_diff + + # @return [Array] + attr_reader :mr_labels + + # Are the changed files only qa specs? + # + # @return [Boolean] whether the changes files are only qa specs + def only_spec_changes? + changed_files.all? { |file_path| file_path =~ SPEC_PATTERN } + end + + # Are the changed files only outside the qa directory? + # + # @return [Boolean] whether the changes files are outside of qa directory + def non_qa_changes? + changed_files.none? { |file_path| file_path =~ QA_PATTERN } + end + + # Extract devops stage from MR labels + # + # @return [String] a devops stage + def devops_stage_from_mr_labels + mr_labels.find { |label| label =~ /^devops::/ }&.delete_prefix('devops::') + end + + # Get qa spec directories for devops stage + # + # @return [Array] qa spec directories + def qa_spec_directories_for_devops_stage + devops_stage = devops_stage_from_mr_labels + return unless devops_stage + + Dir.glob("qa/specs/**/*/").select { |dir| dir =~ %r{\d+_#{devops_stage}/$} } + end + + # Change files in merge request + # + # @return [Array<String>] + def changed_files + @changed_files ||= mr_diff.map { |change| change[:path] } # rubocop:disable Rails/Pluck + end + end + end + end +end diff --git a/qa/qa/tools/ci/test_results.rb b/qa/qa/tools/ci/test_results.rb new file mode 100644 index 00000000000..635b69f6ca0 --- /dev/null +++ b/qa/qa/tools/ci/test_results.rb @@ -0,0 +1,78 @@ +# frozen_string_literal: true + +module QA + module Tools + module Ci + class TestResults + include Helpers + + def initialize(pipeline_name, test_report_job_name, report_path) + @pipeline_name = pipeline_name + @test_report_job_name = test_report_job_name + @report_path = report_path + end + + # Get test report artifacts from downstream pipeline + # + # @param [String] pipeline_name + # @param [String] test_report_job_name + # @param [String] report_path + # @return [void] + def self.get(pipeline_name, test_report_job_name, report_path) + new(pipeline_name, test_report_job_name, report_path).download_test_results + end + + # Download test results from child pipeline + # + # @return [void] + def download_test_results + logger.info("Fetching test results for '#{pipeline_name}'") + + logger.debug(" fetching pipeline id of '#{pipeline_name}' child pipeline") + downstream_pipeline_id = api_get("#{pipelines_url(pipeline_id)}/bridges") + .find { |bridge| bridge[:name] == pipeline_name } + &.dig(:downstream_pipeline, :id) + return logger.error("Child pipeline '#{pipeline_name}' not found!") unless downstream_pipeline_id + + logger.debug(" fetching job id of test report job") + job_id = api_get("#{pipelines_url(downstream_pipeline_id)}/jobs") + .find { |job| job[:name] == test_report_job_name } + &.fetch(:id) + return logger.error("Test report job '#{test_report_job_name}' not found!") unless job_id + + logger.debug(" fetching test results artifact archive") + response = api_get("/projects/#{project_id}/jobs/#{job_id}/artifacts", raw_response: true) + + logger.info("Extracting test result archive") + system("unzip", "-o", "-d", report_path, response.file.path) + end + + private + + attr_reader :pipeline_name, :test_report_job_name, :report_path + + # Base get pipeline url + # + # @param [Integer] id + # @return [String] + def pipelines_url(id) + "/projects/#{project_id}/pipelines/#{id}" + end + + # Current pipeline id + # + # @return [String] + def pipeline_id + ENV["CI_PIPELINE_ID"] + end + + # Current project id + # + # @return [String] + def project_id + ENV["CI_PROJECT_ID"] + end + end + end + end +end diff --git a/qa/qa/tools/revoke_all_personal_access_tokens.rb b/qa/qa/tools/revoke_all_personal_access_tokens.rb deleted file mode 100644 index b4fa02a36d4..00000000000 --- a/qa/qa/tools/revoke_all_personal_access_tokens.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'net/protocol' - -# This script revokes all personal access tokens with the name of 'api-test-token' on the host specified by GITLAB_ADDRESS -# Required environment variables: GITLAB_USERNAME, GITLAB_PASSWORD and GITLAB_ADDRESS -# Run `rake revoke_personal_access_tokens` - -module QA - module Tools - class RevokeAllPersonalAccessTokens - def run - do_run - rescue Net::ReadTimeout - $stdout.puts 'Net::ReadTimeout during run. Trying again' - run - end - - private - - def do_run - raise ArgumentError, "Please provide GITLAB_USERNAME" unless ENV['GITLAB_USERNAME'] - raise ArgumentError, "Please provide GITLAB_PASSWORD" unless ENV['GITLAB_PASSWORD'] - raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] - - $stdout.puts 'Running...' - - Runtime::Browser.visit(ENV['GITLAB_ADDRESS'], Page::Main::Login) - Page::Main::Login.perform(&:sign_in_using_credentials) - Page::Main::Menu.perform(&:click_edit_profile_link) - Page::Profile::Menu.perform(&:click_access_tokens) - - token_name = 'api-test-token' - - Page::Profile::PersonalAccessTokens.perform do |tokens_page| - while tokens_page.has_token_row_for_name?(token_name) - tokens_page.revoke_first_token_with_name(token_name) - print "\e[32m.\e[0m" - end - end - end - end - end -end diff --git a/qa/qa/tools/revoke_user_personal_access_tokens.rb b/qa/qa/tools/revoke_user_personal_access_tokens.rb new file mode 100644 index 00000000000..2854241f420 --- /dev/null +++ b/qa/qa/tools/revoke_user_personal_access_tokens.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +# This script revokes all active personal access tokens owned by a given USER_ID +# up to a given date (Date.today - 1 by default) +# Required environment variables: USER_ID, GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS +# Run `rake revoke_user_pats` + +module QA + module Tools + class RevokeUserPersonalAccessTokens + include Support::API + + def initialize(revoke_before: (Date.today - 1).to_s, dry_run: false) + raise ArgumentError, "Please provide GITLAB_ADDRESS environment variable" unless ENV['GITLAB_ADDRESS'] + + unless ENV['GITLAB_QA_ACCESS_TOKEN'] + raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN environment variable" + end + + raise ArgumentError, "Please provide USER_ID environment variable" unless ENV['USER_ID'] + + @revoke_before = Date.parse(revoke_before) + @dry_run = dry_run + @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], + personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN']) + end + + def run + $stdout.puts 'Running...' + + tokens_head_response = head Runtime::API::Request.new(@api_client, + "/personal_access_tokens?user_id=#{ENV['USER_ID']}", + per_page: "100").url + + total_token_pages = tokens_head_response.headers[:x_total_pages] + total_tokens = tokens_head_response.headers[:x_total] + + $stdout.puts "Total tokens: #{total_tokens}. Total pages: #{total_token_pages}" + + tokens = fetch_tokens + + revoke_tokens(tokens, @api_client, @dry_run) unless tokens.empty? + $stdout.puts "\nDone" + end + + private + + def fetch_tokens + fetched_tokens = [] + + page_no = 1 + + while page_no > 0 + tokens_response = get Runtime::API::Request.new(@api_client, + "/personal_access_tokens?user_id=#{ENV['USER_ID']}", + page: page_no.to_s, per_page: "100").url + + fetched_tokens + .concat(JSON.parse(tokens_response.body) + .select { |token| Date.parse(token["created_at"]) < @revoke_before && token['active'] } + .map { |token| { id: token["id"], name: token["name"], created_at: token["created_at"] } } + ) + + page_no = tokens_response.headers[:x_next_page].to_i + end + + fetched_tokens + end + + def revoke_tokens(tokens, api_client, dry_run = false) + if dry_run + $stdout.puts "Following #{tokens.count} tokens would be revoked:" + else + $stdout.puts "Revoking #{tokens.count} tokens..." + end + + tokens.each do |token| + if dry_run + $stdout.puts "Token name: #{token[:name]}, id: #{token[:id]}, created at: #{token[:created_at]}" + else + request_url = Runtime::API::Request.new(api_client, "/personal_access_tokens/#{token[:id]}").url + + $stdout.puts "\nRevoking token with name: #{token[:name]}, " \ + "id: #{token[:id]}, created at: #{token[:created_at]}" + + delete_response = delete(request_url) + dot_or_f = delete_response.code == 204 ? "\e[32m.\e[0m" : "\e[31mF - #{delete_response}\e[0m" + print dot_or_f + end + end + end + end + end +end diff --git a/qa/qa/tools/test_resources_handler.rb b/qa/qa/tools/test_resources_handler.rb index 0030a47ed55..60c6dbfc16c 100644 --- a/qa/qa/tools/test_resources_handler.rb +++ b/qa/qa/tools/test_resources_handler.rb @@ -27,7 +27,6 @@ module QA include Support::API IGNORED_RESOURCES = [ - 'QA::Resource::PersonalAccessToken', 'QA::Resource::CiVariable', 'QA::Resource::Repository::Commit', 'QA::EE::Resource::GroupIteration', diff --git a/qa/spec/fixtures/ff/bulk_import_projects.yml b/qa/spec/fixtures/ff/bulk_import_projects.yml new file mode 100644 index 00000000000..853389577cf --- /dev/null +++ b/qa/spec/fixtures/ff/bulk_import_projects.yml @@ -0,0 +1,8 @@ +--- +name: bulk_import_projects +introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68873 +rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339941 +milestone: '14.3' +type: development +group: group::import +default_enabled: false diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index f23f4aa728b..195e497f290 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -24,6 +24,10 @@ RSpec.describe QA::Resource::Base do 'block' end + attribute :token do + 'token_value' + end + attribute :username do 'qa' end @@ -208,6 +212,24 @@ RSpec.describe QA::Resource::Base do .to have_received(:debug).with(/api_with_block/) end end + + context 'when the attribute is token and has a block' do + let(:api_resource) { { token: 'another_token_value' } } + + before do + allow(QA::Runtime::Logger).to receive(:debug) + end + + it 'emits a masked debug log entry' do + result = subject.fabricate!(resource: resource) + + expect(result).to be_a(described_class) + expect(result.token).to eq('another_token_value') + + expect(QA::Runtime::Logger) + .to have_received(:debug).with(/MASKED/) + end + end end context 'when the attribute is populated via direct assignment' do diff --git a/qa/spec/resource/user_spec.rb b/qa/spec/resource/user_spec.rb index e7397d9c0bf..c0140abf298 100644 --- a/qa/spec/resource/user_spec.rb +++ b/qa/spec/resource/user_spec.rb @@ -23,14 +23,15 @@ RSpec.describe QA::Resource::User do end describe '#password' do - it 'generates a default password' do - expect(subject.password).to eq('password') + it 'generates a random 16 character password by default' do + expect(subject.password).to match(/\w{16}/) end it 'is possible to set the password' do - subject.password = 'secret' + new_password = "21c7a808" + subject.password = new_password - expect(subject.password).to eq('secret') + expect(subject.password).to eq(new_password) end end diff --git a/qa/spec/specs/helpers/context_selector_spec.rb b/qa/spec/specs/helpers/context_selector_spec.rb index 5a320cde71f..7541bb45d82 100644 --- a/qa/spec/specs/helpers/context_selector_spec.rb +++ b/qa/spec/specs/helpers/context_selector_spec.rb @@ -57,6 +57,26 @@ RSpec.describe QA::Specs::Helpers::ContextSelector do expect(described_class.context_matches?(:production)).to be_truthy end + it 'matches domain' do + QA::Runtime::Scenario.define(:gitlab_address, 'https://jihulab.com') + + aggregate_failures do + expect(described_class.context_matches?(:production)).to be_falsey + expect(described_class.context_matches?(domain: 'gitlab')).to be_falsey + expect(described_class.context_matches?(domain: 'jihulab')).to be_truthy + end + end + + it 'matches tld' do + QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.cn') + + aggregate_failures do + expect(described_class.context_matches?).to be_falsey + expect(described_class.context_matches?(tld: 'net')).to be_falsey + expect(described_class.context_matches?(tld: 'cn')).to be_truthy + end + end + it 'doesnt match with mismatching switches' do QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.test') diff --git a/qa/spec/tools/ci/ff_changes_spec.rb b/qa/spec/tools/ci/ff_changes_spec.rb new file mode 100644 index 00000000000..71ca26867e0 --- /dev/null +++ b/qa/spec/tools/ci/ff_changes_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +RSpec.describe QA::Tools::Ci::FfChanges do + subject(:ff_changes) { described_class.new(mr_diff) } + + before do + allow(Gitlab::QA::TestLogger).to receive(:logger).and_return(Logger.new(StringIO.new)) + end + + context "with merge request pipeline" do + let(:deleted_file) { false } + let(:mr_diff) do + [ + { + path: "config/feature_flags/development/bulk_import_projects.yml", + deleted_file: deleted_file + } + ] + end + + before do + allow(File).to receive(:read) + .with(File.expand_path("../#{mr_diff.first[:path]}", QA::Runtime::Path.qa_root)) + .and_return(File.read("spec/fixtures/ff/bulk_import_projects.yml")) + end + + context "with changed feature flag" do + it "returns inverse ff state option" do + expect(ff_changes.fetch).to eq("bulk_import_projects=enabled") + end + end + + context "with deleted feature flag" do + let(:deleted_file) { true } + + it "returns deleted ff state option" do + expect(ff_changes.fetch).to eq("bulk_import_projects=deleted") + end + end + end + + context "without merge request pipeline" do + let(:mr_diff) { [] } + + context "with empty mr diff" do + it "doesn't return any ff options" do + expect(ff_changes.fetch).to be_nil + end + end + end +end diff --git a/qa/spec/tools/ci/non_empty_suites_spec.rb b/qa/spec/tools/ci/non_empty_suites_spec.rb new file mode 100644 index 00000000000..d9bfe1eebe7 --- /dev/null +++ b/qa/spec/tools/ci/non_empty_suites_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +RSpec.describe QA::Tools::Ci::NonEmptySuites do + let(:non_empty_suites) { described_class.new(nil) } + + let(:status) { instance_double(Process::Status, success?: true) } + + before do + allow(Gitlab::QA::TestLogger).to receive(:logger).and_return(Logger.new(StringIO.new)) + allow(Open3).to receive(:capture3).and_return(["output\n0", "", status]) + allow(Open3).to receive(:capture3) + .with("bundle exec bin/qa Test::Instance::All --count-examples-only --address http://dummy1.test") + .and_return(["output\n1", "", status]) + end + + it "returns runnable test suites" do + expect(non_empty_suites.fetch).to eq("Test::Instance::All") + end +end diff --git a/qa/spec/tools/ci/qa_changes_spec.rb b/qa/spec/tools/ci/qa_changes_spec.rb new file mode 100644 index 00000000000..bc98ec16d7f --- /dev/null +++ b/qa/spec/tools/ci/qa_changes_spec.rb @@ -0,0 +1,87 @@ +# frozen_string_literal: true + +RSpec.describe QA::Tools::Ci::QaChanges do + subject(:qa_changes) { described_class.new(mr_diff, mr_labels) } + + let(:mr_labels) { [] } + + before do + allow(File).to receive(:directory?).and_return(false) + end + + context "with spec only changes" do + let(:mr_diff) do + [ + { path: "qa/qa/specs/features/test_spec.rb", diff: "" }, + { path: "qa/qa/specs/features/another_test_spec.rb", diff: "" } + ] + end + + it ".qa_tests return changed specs" do + expect(qa_changes.qa_tests).to eq( + "qa/specs/features/test_spec.rb qa/specs/features/another_test_spec.rb" + ) + end + + it ".framework_changes? return false" do + expect(qa_changes.framework_changes?).to eq(false) + end + + it ".quarantine_changes? return false" do + expect(qa_changes.quarantine_changes?).to eq(false) + end + end + + context "with framework changes" do + let(:mr_diff) { [{ path: "qa/qa.rb" }] } + + it ".qa_tests do not return specifix specs" do + expect(qa_changes.qa_tests).to be_nil + end + + it ".framework_changes? return true" do + expect(qa_changes.framework_changes?).to eq(true) + end + + it ".quarantine_changes? return false" do + expect(qa_changes.quarantine_changes?).to eq(false) + end + end + + context "with non qa changes" do + let(:mr_diff) { [{ path: "Gemfile" }] } + + it ".framework_changes? return false" do + expect(qa_changes.framework_changes?).to eq(false) + end + + it ".quarantine_changes? return false" do + expect(qa_changes.quarantine_changes?).to eq(false) + end + + context "without mr labels" do + it ".qa_tests do not return any specific specs" do + expect(qa_changes.qa_tests).to be_nil + end + end + + context "with mr label" do + let(:mr_labels) { ["devops::manage"] } + + it ".qa_tests return specs for devops stage" do + expect(qa_changes.qa_tests.split(" ")).to include( + "qa/specs/features/browser_ui/1_manage/", + "qa/specs/features/api/1_manage/" + ) + end + end + end + + context "with quarantine changes" do + let(:mr_diff) { [{ path: "qa/qa/specs/features/test_spec.rb", diff: "+ , quarantine: true" }] } + + it ".quarantine_changes? return true" do + expect(qa_changes.quarantine_changes?).to eq(true) + end + end +end diff --git a/qa/tasks/ci.rake b/qa/tasks/ci.rake new file mode 100644 index 00000000000..44a794d9f94 --- /dev/null +++ b/qa/tasks/ci.rake @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +require_relative "helpers/util" + +# rubocop:disable Rails/RakeEnvironment +namespace :ci do + include Task::Helpers::Util + + desc "Detect changes and populate test variables for selective test execution and feature flag testing" + task :detect_changes, [:env_file] do |_, args| + env_file = args[:env_file] + abort("ERROR: Path for environment file must be provided") unless env_file + + diff = mr_diff + labels = mr_labels + + qa_changes = QA::Tools::Ci::QaChanges.new(diff, labels) + logger = qa_changes.logger + + logger.info("Analyzing merge request changes") + # skip running tests when only quarantine changes detected + if qa_changes.quarantine_changes? + logger.info(" merge request contains only quarantine changes, e2e test execution will be skipped!") + append_to_file(env_file, <<~TXT) + QA_SKIP_ALL_TESTS=true + TXT + next + end + + tests = qa_changes.qa_tests + if qa_changes.framework_changes? # run all tests when framework changes detected + logger.info(" merge request contains qa framework changes, full test suite will be executed") + append_to_file(env_file, <<~TXT) + QA_FRAMEWORK_CHANGES=true + TXT + elsif tests + logger.info(" detected following specs to execute: '#{tests}'") + else + logger.info(" no specific specs to execute detected") + end + + # always check all test suites in case a suite is defined but doesn't have any runnable specs + suites = QA::Tools::Ci::NonEmptySuites.new(tests).fetch + append_to_file(env_file, <<~TXT) + QA_TESTS='#{tests}' + QA_SUITES='#{suites}' + TXT + + # check if mr contains feature flag changes + feature_flags = QA::Tools::Ci::FfChanges.new(diff).fetch + append_to_file(env_file, <<~TXT) + QA_FEATURE_FLAGS='#{feature_flags}' + TXT + end + + desc "Download test results from downstream pipeline" + task :download_test_results, [:trigger_name, :test_report_job_name, :report_path] do |_, args| + QA::Tools::Ci::TestResults.get(args[:trigger_name], args[:test_report_job_name], args[:report_path]) + end +end +# rubocop:enable Rails/RakeEnvironment diff --git a/qa/tasks/helpers/util.rb b/qa/tasks/helpers/util.rb new file mode 100644 index 00000000000..f8eb9b02f72 --- /dev/null +++ b/qa/tasks/helpers/util.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module Task + module Helpers + module Util + include ::QA::Support::API + + # Append text to file + # + # @param [String] path + # @param [String] text + # @return [void] + def append_to_file(path, text) + File.open(path, "a") { |f| f.write(text) } + end + + # Merge request labels + # + # @return [Array] + def mr_labels + ENV["CI_MERGE_REQUEST_LABELS"]&.split(',') || [] + end + + # Merge request changes + # + # @return [Array<Hash>] + def mr_diff + mr_iid = ENV["CI_MERGE_REQUEST_IID"] + return [] unless mr_iid + + gitlab_endpoint = ENV["CI_API_V4_URL"] + gitlab_token = ENV["PROJECT_TOKEN_FOR_CI_SCRIPTS_API_USAGE"] + project_id = ENV["CI_MERGE_REQUEST_PROJECT_ID"] + + response = get( + "#{gitlab_endpoint}/projects/#{project_id}/merge_requests/#{mr_iid}/changes", + headers: { "PRIVATE-TOKEN" => gitlab_token } + ) + + parse_body(response).fetch(:changes, []).map do |change| + { + path: change[:new_path], + **change.slice(:new_file, :deleted_file, :diff) + } + end + end + end + end +end |