diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 08:17:02 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-08-18 08:17:02 +0000 |
commit | b39512ed755239198a9c294b6a45e65c05900235 (patch) | |
tree | d234a3efade1de67c46b9e5a38ce813627726aa7 /qa | |
parent | d31474cf3b17ece37939d20082b07f6657cc79a9 (diff) | |
download | gitlab-ce-b39512ed755239198a9c294b6a45e65c05900235.tar.gz |
Add latest changes from gitlab-org/gitlab@15-3-stable-eev15.3.0-rc42
Diffstat (limited to 'qa')
125 files changed, 2164 insertions, 759 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile index 9611b3653eb..341732ab56f 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,5 +1,5 @@ ARG DOCKER_VERSION=20.10.14 -ARG CHROME_VERSION=101 +ARG CHROME_VERSION=103 ARG QA_BUILD_TARGET=qa FROM registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23 AS qa diff --git a/qa/Gemfile b/qa/Gemfile index d8d00400563..7c46d35bb48 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -29,12 +29,16 @@ gem 'influxdb-client', '~> 1.17' gem 'terminal-table', '~> 3.0.0', require: false gem 'slack-notifier', '~> 2.4', require: false gem 'fog-google', '~> 1.17', require: false +gem "warning", "~> 1.3" gem 'confiner', '~> 0.3' gem 'chemlab', '~> 0.9' gem 'chemlab-library-www-gitlab-com', '~> 0.1' +# dependencies for jenkins client +gem 'nokogiri', '~> 1.12' + gem 'deprecation_toolkit', '~> 1.5.1', require: false group :development do diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 71484c30c9a..ff382788c5a 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -148,8 +148,8 @@ GEM google-apis-core (>= 0.4, < 2.a) google-apis-storage_v1 (0.9.0) google-apis-core (>= 0.4, < 2.a) - google-cloud-env (1.5.0) - faraday (>= 0.17.3, < 2.0) + google-cloud-env (1.6.0) + faraday (>= 0.17.3, < 3.0) googleauth (1.1.0) faraday (>= 0.17.3, < 2.0) jwt (>= 1.4, < 3.0) @@ -198,12 +198,12 @@ GEM multi_xml (0.6.0) multipart-post (2.1.1) netrc (0.11.0) - nokogiri (1.13.6) + nokogiri (1.13.8) mini_portile2 (~> 2.8.0) racc (~> 1.4) - octokit (4.21.0) - faraday (>= 0.9) - sawyer (~> 0.8.0, >= 0.5.3) + octokit (4.25.1) + faraday (>= 1, < 3) + sawyer (~> 0.9) oj (3.13.11) os (1.1.4) parallel (1.19.2) @@ -270,9 +270,9 @@ GEM rake (>= 0.8.1) ruby2_keywords (0.0.4) rubyzip (2.3.2) - sawyer (0.8.2) + sawyer (0.9.2) addressable (>= 2.3.5) - faraday (> 0.8, < 2.0) + faraday (>= 0.17.3, < 3) selenium-webdriver (4.0.3) childprocess (>= 0.5, < 5.0) rexml (~> 3.2, >= 3.2.5) @@ -307,6 +307,7 @@ GEM procto (~> 0.0.2) uuid (2.3.9) macaddr (~> 1.0) + warning (1.3.0) watir (6.19.1) regexp_parser (>= 1.2, < 3) selenium-webdriver (>= 3.142.7) @@ -337,6 +338,7 @@ DEPENDENCIES gitlab-qa (~> 7) influxdb-client (~> 1.17) knapsack (~> 4.0) + nokogiri (~> 1.12) octokit (~> 4.21) parallel (~> 1.19) parallel_tests (~> 2.29) @@ -354,6 +356,7 @@ DEPENDENCIES slack-notifier (~> 2.4) terminal-table (~> 3.0.0) timecop (~> 0.9.1) + warning (~> 1.3) webdrivers (~> 5.0) zeitwerk (~> 2.4) diff --git a/qa/Rakefile b/qa/Rakefile index e4d38d8294f..d3e39d8ed1e 100644 --- a/qa/Rakefile +++ b/qa/Rakefile @@ -1,5 +1,4 @@ # frozen_string_literal: true -# rubocop:disable Rails/RakeEnvironment require_relative "qa" @@ -86,4 +85,8 @@ namespace :test_resources do QA::Tools::TestResourcesHandler.new.download(args[:ci_project_name]) end end -# rubocop:enable Rails/RakeEnvironment + +desc "Deletes user's projects" +task :delete_user_projects, [:delete_before, :dry_run] do |t, args| + QA::Tools::DeleteUserProjects.new(args).run +end @@ -17,8 +17,6 @@ require 'active_support/core_ext/hash' require 'active_support/core_ext/object/blank' require 'rainbow/refinement' -require_relative 'qa/support/fips' - module QA root = "#{__dir__}/qa" @@ -67,7 +65,8 @@ module QA "registry_tls" => "RegistryTLS", "jetbrains" => "JetBrains", "vscode" => "VSCode", - "registry_with_cdn" => "RegistryWithCDN" + "registry_with_cdn" => "RegistryWithCDN", + "fips" => "FIPS" ) # Configure knapsack at the very begining of the setup @@ -77,3 +76,10 @@ module QA loader.setup end + +# Custom warning processing +Warning.process do |warning| + QA::Runtime::Logger.warn(warning.strip) +end + +Warning.ignore(/already initialized constant Chemlab::Vendor|previous definition of Vendor was here/) diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb index bf08f887c7d..981b60d1920 100644 --- a/qa/qa/ce/strategy.rb +++ b/qa/qa/ce/strategy.rb @@ -6,6 +6,16 @@ module QA extend self def perform_before_hooks + if QA::Runtime::Env.admin_personal_access_token.present? + QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::User.admin_username, + QA::Runtime::Env.admin_personal_access_token) + end + + if QA::Runtime::Env.personal_access_token.present? && QA::Runtime::Env.user_username.present? + QA::Resource::PersonalAccessTokenCache.set_token_for_username(QA::Runtime::Env.user_username, + QA::Runtime::Env.personal_access_token) + end + # The login page could take some time to load the first time it is visited. # We visit the login page and wait for it to properly load only once before the tests. QA::Runtime::Logger.info("Performing sanity check for environment!") diff --git a/qa/qa/flow/purchase.rb b/qa/qa/flow/purchase.rb index 5558e177685..e0efa8a8178 100644 --- a/qa/qa/flow/purchase.rb +++ b/qa/qa/flow/purchase.rb @@ -108,3 +108,5 @@ module QA end end end + +QA::Flow::Purchase.prepend_mod_with('Flow::Purchase', namespace: QA) diff --git a/qa/qa/page/admin/overview/users/components/impersonation_tokens.rb b/qa/qa/page/admin/overview/users/components/impersonation_tokens.rb new file mode 100644 index 00000000000..0d0c92ce29d --- /dev/null +++ b/qa/qa/page/admin/overview/users/components/impersonation_tokens.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module QA + module Page + module Admin + module Overview + module Users + module Components + class ImpersonationTokens < Page::Base + include Page::Component::AccessTokens + include Page::Component::ConfirmModal + end + end + end + end + end + end +end diff --git a/qa/qa/page/admin/overview/users/show.rb b/qa/qa/page/admin/overview/users/show.rb index be73f3d80bf..2fde3ac2c6d 100644 --- a/qa/qa/page/admin/overview/users/show.rb +++ b/qa/qa/page/admin/overview/users/show.rb @@ -8,6 +8,7 @@ module QA class Show < QA::Page::Base view 'app/views/admin/users/_head.html.haml' do element :impersonate_user_link + element :impersonation_tokens_tab end view 'app/views/admin/users/show.html.haml' do @@ -32,6 +33,11 @@ module QA click_element(:user_actions_dropdown_toggle, username: user.username) end + def go_to_impersonation_tokens(&block) + navigate_to_tab(:impersonation_tokens_tab) + Users::Components::ImpersonationTokens.perform(&block) + end + def click_impersonate_user click_element(:impersonate_user_link) end @@ -50,6 +56,20 @@ module QA click_element :approve_user_button click_element :approve_user_confirm_button end + + private + + def navigate_to_tab(element_name) + wait_until(reload: false) do + click_element element_name unless on_impersontation_tokens_tab? + + on_impersontation_tokens_tab?(wait: 10) + end + end + + def on_impersontation_tokens_tab?(wait: 1) + has_css?(".active", text: 'Impersonation Tokens', wait: wait) + end end end end diff --git a/qa/qa/page/alert/auto_devops_alert.rb b/qa/qa/page/alert/auto_devops_alert.rb index 8f66c805b77..26801c4996c 100644 --- a/qa/qa/page/alert/auto_devops_alert.rb +++ b/qa/qa/page/alert/auto_devops_alert.rb @@ -5,7 +5,7 @@ module QA module Alert class AutoDevopsAlert < Page::Base view 'app/views/shared/_auto_devops_implicitly_enabled_banner.html.haml' do - element :auto_devops_banner + element :auto_devops_banner_content end end end diff --git a/qa/qa/page/component/access_tokens.rb b/qa/qa/page/component/access_tokens.rb index f143e5b9e1f..36c0f8c2f00 100644 --- a/qa/qa/page/component/access_tokens.rb +++ b/qa/qa/page/component/access_tokens.rb @@ -23,6 +23,10 @@ module QA element :create_token_button end + base.view 'app/views/shared/access_tokens/_table.html.haml' do + element :revoke_button + end + base.view 'app/views/shared/tokens/_scopes_form.html.haml' do element :api_label, '#{scope}_label' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck end @@ -54,7 +58,11 @@ module QA def fill_expiry_date(date) date = date.to_s if date.is_a?(Date) - Date.strptime(date, '%Y-%m-%d') rescue ArgumentError raise "Expiry date must be in YYYY-MM-DD format" + begin + Date.strptime(date, '%Y-%m-%d') + rescue ArgumentError + raise "Expiry date must be in YYYY-MM-DD format" + end fill_element(:expiry_date_field, date) end diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb index 4c053f1d6a9..2ba198621fc 100644 --- a/qa/qa/page/component/ci_badge_link.rb +++ b/qa/qa/page/component/ci_badge_link.rb @@ -40,8 +40,6 @@ module QA find_element(:status_badge).text end - private - def completed?(timeout: 60) wait_until(reload: false, sleep_interval: 3.0, max_duration: timeout) do COMPLETED_STATUSES.include?(status_badge) diff --git a/qa/qa/page/component/confirm_modal.rb b/qa/qa/page/component/confirm_modal.rb index 76200490f66..4d1cf30c392 100644 --- a/qa/qa/page/component/confirm_modal.rb +++ b/qa/qa/page/component/confirm_modal.rb @@ -12,21 +12,26 @@ module QA base.view 'app/assets/javascripts/lib/utils/confirm_via_gl_modal/confirm_modal.vue' do element :confirm_ok_button end + + base.view 'app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger_modal.vue' do + element :confirm_danger_modal_button + element :confirm_danger_field + end end def fill_confirmation_text(text) - fill_element(:confirm_input, text) + fill_element(:confirm_danger_field, text) end def wait_for_confirm_button_enabled wait_until(reload: false) do - !find_element(:confirm_button).disabled? + !find_element(:confirm_danger_modal_button).disabled? end end def confirm_transfer wait_for_confirm_button_enabled - click_element(:confirm_button) + click_element(:confirm_danger_modal_button) end def click_confirmation_ok_button diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb index f82bb81a3fc..ff61c91f0f6 100644 --- a/qa/qa/page/component/groups_filter.rb +++ b/qa/qa/page/component/groups_filter.rb @@ -10,7 +10,7 @@ module QA super base.view 'app/views/shared/groups/_search_form.html.haml' do - element :groups_filter + element :groups_filter_field end base.view 'app/assets/javascripts/groups/components/groups.vue' do @@ -22,7 +22,7 @@ module QA def has_filtered_group?(name) # Filter and submit to reload the page and only retrieve the filtered results - find_element(:groups_filter).set(name).send_keys(:return) + find_element(:groups_filter_field).set(name).send_keys(:return) # Since we submitted after filtering, the presence of # groups_list_tree_container means we have the complete filtered list diff --git a/qa/qa/page/component/issuable/sidebar.rb b/qa/qa/page/component/issuable/sidebar.rb index 4131731111f..68da89dc81d 100644 --- a/qa/qa/page/component/issuable/sidebar.rb +++ b/qa/qa/page/component/issuable/sidebar.rb @@ -35,7 +35,7 @@ module QA end base.view 'app/views/shared/issuable/_sidebar.html.haml' do - element :assignee_block + element :assignee_block_container element :milestone_block end @@ -127,7 +127,7 @@ module QA private def wait_assignees_block_finish_loading - within_element(:assignee_block) do + within_element(:assignee_block_container) do wait_until(reload: false, max_duration: 10, sleep_interval: 1) do finished_loading_block? yield diff --git a/qa/qa/page/component/namespace_select.rb b/qa/qa/page/component/namespace_select.rb index 924e1af876c..4dbcb39ced6 100644 --- a/qa/qa/page/component/namespace_select.rb +++ b/qa/qa/page/component/namespace_select.rb @@ -13,6 +13,7 @@ module QA element :namespaces_list element :namespaces_list_groups element :namespaces_list_item + element :namespaces_list_search end end @@ -20,6 +21,10 @@ module QA click_element :namespaces_list within_element(:namespaces_list) do + find_element(:namespaces_list_search).fill_in(with: item) + + wait_for_requests + find_element(:namespaces_list_item, text: item).click end end diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb index 74b6c6b2d5e..22b9a4c8b0d 100644 --- a/qa/qa/page/component/wiki_page_form.rb +++ b/qa/qa/page/component/wiki_page_form.rb @@ -48,7 +48,9 @@ module QA end def use_new_editor - click_element(:editing_mode_button, mode: 'Edit rich text') + within_element(:editing_mode_button) do + find('label', text: 'Rich text').click + end wait_until(reload: false) do has_element?(:content_editor_container) diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb index 52853376f17..644d19d6bcb 100644 --- a/qa/qa/page/dashboard/groups.rb +++ b/qa/qa/page/dashboard/groups.rb @@ -6,13 +6,8 @@ module QA class Groups < Page::Base include Page::Component::GroupsFilter - view 'app/views/shared/groups/_search_form.html.haml' do - element :groups_filter, 'search_field_tag :filter' # rubocop:disable QA/ElementWithPattern - element :groups_filter_placeholder, 'Search by name' # rubocop:disable QA/ElementWithPattern - end - view 'app/views/dashboard/_groups_head.html.haml' do - element :new_group_button, 'link_to _("New group")' # rubocop:disable QA/ElementWithPattern + element :new_group_button end def has_group?(name) @@ -26,7 +21,7 @@ module QA end def click_new_group - click_on 'New group' + click_element(:new_group_button) end end end diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index a0b42598962..10529ed69e1 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -5,7 +5,7 @@ module QA module Dashboard class Projects < Page::Base view 'app/views/shared/projects/_search_form.html.haml' do - element :project_filter_form, required: true + element :project_filter_form_container, required: true end view 'app/views/shared/projects/_project.html.haml' do @@ -24,7 +24,7 @@ module QA end def filter_by_name(name) - within_element(:project_filter_form) do + within_element(:project_filter_form_container) do fill_in :name, with: name end end @@ -44,7 +44,7 @@ module QA end def clear_project_filter - fill_element(:project_filter_form, "") + fill_element(:project_filter_form_container, "") end end end diff --git a/qa/qa/page/file/edit.rb b/qa/qa/page/file/edit.rb index 3a4a1837b1c..d2b8c8260fd 100644 --- a/qa/qa/page/file/edit.rb +++ b/qa/qa/page/file/edit.rb @@ -7,6 +7,30 @@ module QA include Shared::CommitMessage include Shared::CommitButton include Shared::Editor + + view 'app/assets/javascripts/editor/components/source_editor_toolbar_button.vue' do + element :editor_toolbar_button + end + + view 'app/views/projects/blob/_editor.html.haml' do + element :source_editor_preview_container + end + + def has_markdown_preview?(component, content) + within_element(:source_editor_preview_container) do + has_css?(component, exact_text: content) + end + end + + def wait_for_markdown_preview(component, content) + return if has_markdown_preview?(component, content) + + raise ElementNotFound, %("Couldn't find #{component} element with content '#{content}') + end + + def click_editor_toolbar + click_element(:editor_toolbar_button) + end end end end diff --git a/qa/qa/page/file/shared/editor.rb b/qa/qa/page/file/shared/editor.rb index ce4465d2a5c..dab02c1e34f 100644 --- a/qa/qa/page/file/shared/editor.rb +++ b/qa/qa/page/file/shared/editor.rb @@ -20,7 +20,11 @@ module QA end def remove_content - text_area.send_keys([:command, 'a'], :backspace) + if page.driver.browser.capabilities.platform.include? "mac" + text_area.send_keys([:command, 'a'], :backspace) + else + text_area.send_keys([:control, 'a'], :backspace) + end end private diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index 09a9af7aaf7..1f17b470ada 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -12,7 +12,7 @@ module QA end view 'app/views/groups/_new_group_fields.html.haml' do - element :create_group_button, "submit _('Create group')" # rubocop:disable QA/ElementWithPattern + element :create_group_button end view 'app/views/groups/_import_group_from_another_instance_panel.html.haml' do @@ -34,6 +34,10 @@ module QA click_button 'Create group' end + def create_subgroup + click_button 'Create subgroup' + end + def set_gitlab_url(url) fill_element(:import_gitlab_url, url) end diff --git a/qa/qa/page/issuable/new.rb b/qa/qa/page/issuable/new.rb index 0c95f722080..f3e6a84ef54 100644 --- a/qa/qa/page/issuable/new.rb +++ b/qa/qa/page/issuable/new.rb @@ -5,11 +5,7 @@ module QA module Issuable class New < Page::Base view 'app/views/shared/issuable/form/_title.html.haml' do - element :issuable_form_title - end - - view 'app/views/shared/issuable/form/_metadata.html.haml' do - element :issuable_milestone_dropdown + element :issuable_form_title_field end view 'app/views/shared/form_elements/_description.html.haml' do @@ -17,11 +13,12 @@ module QA end view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do - element :issuable_dropdown_menu_milestone + element :issuable_milestone_dropdown + element :issuable_milestone_dropdown_content end view 'app/views/shared/issuable/_label_dropdown.html.haml' do - element :issuable_label + element :issuable_label_dropdown end view 'app/views/shared/issuable/form/_metadata_issuable_assignee.html.haml' do @@ -33,7 +30,7 @@ module QA end def fill_title(title) - fill_element :issuable_form_title, title + fill_element :issuable_form_title_field, title end def fill_description(description) @@ -42,7 +39,7 @@ module QA def choose_milestone(milestone) click_element :issuable_milestone_dropdown - within_element(:issuable_dropdown_menu_milestone) do + within_element(:issuable_milestone_dropdown_content) do click_on milestone.title end end @@ -55,11 +52,11 @@ module QA end def select_label(label) - click_element :issuable_label + click_element :issuable_label_dropdown click_link label.title - click_element :issuable_label # So that the dropdown goes away(click away action) + click_element :issuable_label_dropdown # So that the dropdown goes away(click away action) end def assign_to_me diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb index e73d40b37ac..e19bc0838c9 100644 --- a/qa/qa/page/label/index.rb +++ b/qa/qa/page/label/index.rb @@ -7,26 +7,26 @@ module QA include Component::LazyLoader view 'app/views/shared/labels/_nav.html.haml' do - element :label_create_new + element :create_new_label_button end view 'app/views/shared/empty_states/_labels.html.haml' do - element :label_svg + element :label_svg_content end view 'app/views/shared/empty_states/_priority_labels.html.haml' do - element :label_svg + element :label_svg_content end def click_new_label_button # The 'labels.svg' takes a fraction of a second to load after which the "New label" button shifts up a bit # This can cause webdriver to miss the hit so we wait for the svg to load (implicitly with has_element?) # before clicking the button. - within_element(:label_svg) do + within_element(:label_svg_content) do has_element?(:js_lazy_loaded) end - click_element :label_create_new + click_element :create_new_label_button end end end diff --git a/qa/qa/page/label/new.rb b/qa/qa/page/label/new.rb index a40179489c1..12427443280 100644 --- a/qa/qa/page/label/new.rb +++ b/qa/qa/page/label/new.rb @@ -5,9 +5,9 @@ module QA module Label class New < Page::Base view 'app/views/shared/labels/_form.html.haml' do - element :label_title - element :label_description - element :label_color + element :label_title_field + element :label_description_field + element :label_color_field element :label_create_button end @@ -16,15 +16,15 @@ module QA end def fill_title(title) - fill_element :label_title, title + fill_element :label_title_field, title end def fill_description(description) - fill_element :label_description, description + fill_element :label_description_field, description end def fill_color(color) - fill_element :label_color, color + fill_element :label_color_field, color end end end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index f3ee627c41e..d7ca8223862 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -183,8 +183,7 @@ module QA switch_to_sign_in_tab if has_sign_in_tab? switch_to_standard_tab if has_standard_tab? - fill_element :login_field, user.username - fill_element :password_field, user.password + fill_in_credential(user) if Runtime::Env.running_on_dot_com? click_accept_all_cookies if has_accept_all_cookies_button? @@ -211,6 +210,11 @@ module QA Page::Main::Menu.validate_elements_present! unless skip_page_validation end + def fill_in_credential(user) + fill_element :login_field, user.username + fill_element :password_field, user.password + end + # Handle request for password change # Happens on clean GDK installations when seeded root admin password is expired # @@ -236,3 +240,5 @@ module QA end end end + +QA::Page::Main::Login.prepend_mod_with('Page::Main::Login', namespace: QA) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 27c12a4e21f..9fc0cf0ccf8 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -11,6 +11,7 @@ module QA element :review_preview_dropdown end + # Remove once :mr_review_submit_comment ff is enabled by default view 'app/assets/javascripts/batch_comments/components/publish_button.vue' do element :submit_review_button end @@ -19,8 +20,9 @@ module QA element :review_bar_content end - view 'app/assets/javascripts/batch_comments/components/draft_note.vue' do - element :draft_note_content + view 'app/assets/javascripts/batch_comments/components/submit_dropdown.vue' do + element :submit_review_dropdown + element :submit_review_button end view 'app/assets/javascripts/diffs/components/compare_dropdown_layout.vue' do @@ -71,7 +73,6 @@ module QA view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do element :cherry_pick_button - element :merged_status_content element :revert_button end @@ -134,6 +135,11 @@ module QA element :cancel_auto_merge_button end + view 'app/views/shared/_broadcast_message.html.haml' do + element :broadcast_notification_container + element :close_button + end + def start_review click_element(:start_review_button) @@ -151,16 +157,34 @@ module QA end def submit_pending_reviews - has_element?(:submit_review_button) + # On test environments we have a broadcast message that can cover the buttons + + if has_element?(:broadcast_notification_container, wait: 5) + within_element(:broadcast_notification_container) do + click_element(:close_button) + end + end + within_element(:review_bar_content) do click_element(:review_preview_dropdown) end - within_element(:draft_note_content) do + + # Remove if statement once :mr_review_submit_comment ff is enabled by default + + if has_element?(:submit_review_dropdown, wait: 5) + click_element(:submit_review_dropdown) click_element(:submit_review_button) + else + within_element(:review_bar_content) do + click_element(:submit_review_button) + end end - # After clicking the button, wait for it to disappear + + # After clicking the button, wait for the review bar to disappear # before moving on to the next part of the test - has_no_element?(:submit_review_button) + wait_until(reload: false) do + has_no_element?(:review_bar_content) + end end def add_comment_to_diff(text) diff --git a/qa/qa/page/profile/ssh_keys.rb b/qa/qa/page/profile/ssh_keys.rb index 8da484003f4..db71062cec6 100644 --- a/qa/qa/page/profile/ssh_keys.rb +++ b/qa/qa/page/profile/ssh_keys.rb @@ -31,7 +31,11 @@ module QA def fill_expiry_date(date) date = date.strftime('%m/%d/%Y') if date.is_a?(Date) - Date.strptime(date, '%m/%d/%Y') rescue ArgumentError raise "Expiry date must be in mm/dd/yyyy format" + begin + Date.strptime(date, '%m/%d/%Y') + rescue ArgumentError + raise "Expiry date must be in mm/dd/yyyy format" + end fill_element(:key_expiry_date_field, date) end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 7864e664429..bb4fb84f2d2 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -81,9 +81,9 @@ module QA # Disable experiment for SAST at project creation https://gitlab.com/gitlab-org/gitlab/-/issues/333196 def disable_initialize_with_sast - return unless has_element?(:initialize_with_sast_checkbox) + return unless has_element?(:initialize_with_sast_checkbox, visible: false) - uncheck_element(:initialize_with_sast_checkbox) + uncheck_element(:initialize_with_sast_checkbox, true) end def click_github_link @@ -95,7 +95,7 @@ module QA end def disable_initialize_with_readme - uncheck_element(:initialize_with_readme_checkbox) + uncheck_element(:initialize_with_readme_checkbox, true) end end end diff --git a/qa/qa/page/project/pipeline_editor/show.rb b/qa/qa/page/project/pipeline_editor/show.rb index 78f16a8a65c..70c0c5abb52 100644 --- a/qa/qa/page/project/pipeline_editor/show.rb +++ b/qa/qa/page/project/pipeline_editor/show.rb @@ -54,7 +54,12 @@ module QA element :file_tree_popover end + view 'app/assets/javascripts/pipeline_editor/components/validate/ci_validate.vue' do + element :simulate_pipeline_button + end + def initialize + wait_for_requests(skip_finished_loading_check: true) dismiss_file_tree_popover if has_element?(:file_tree_popover) super @@ -89,7 +94,7 @@ module QA end def submit_changes - Support::Waiter.wait_until { !find_element(:commit_changes_button).disabled? } + wait_until(reload: false) { !find_element(:commit_changes_button).disabled? } click_element(:commit_changes_button) wait_for_requests @@ -115,14 +120,14 @@ module QA go_to_tab('Visualize') end - def go_to_lint_tab - go_to_tab('Lint') - end - def go_to_view_merged_yaml_tab go_to_tab('View merged YAML') end + def go_to_validate_tab + go_to_tab('Validate') + end + def has_source_editor? has_element?(:source_editor_container) end @@ -141,6 +146,12 @@ module QA end end + def tab_alert_title + within_element(:file_editor_container) do + find('.gl-alert-title').text + end + end + def has_new_mr_checkbox? has_element?(:new_mr_checkbox, visible: true) end @@ -153,6 +164,10 @@ module QA check_element(:new_mr_checkbox, true) end + def simulate_pipeline + click_element(:simulate_pipeline_button) + end + private def go_to_tab(name) diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb index fd9cc8a13e7..9d8ed132ffd 100644 --- a/qa/qa/page/project/settings/advanced.rb +++ b/qa/qa/page/project/settings/advanced.rb @@ -8,6 +8,10 @@ module QA include QA::Page::Component::ConfirmModal include Component::NamespaceSelect + view 'app/assets/javascripts/vue_shared/components/confirm_danger/confirm_danger.vue' do + element :confirm_danger_button + end + view 'app/views/projects/edit.html.haml' do element :project_path_field element :change_path_button @@ -47,7 +51,7 @@ module QA # https://gitlab.com/gitlab-org/gitlab/-/issues/218965 select_namespace(namespace.gsub(%r{([^\s])/([^\s])}, '\1 / \2')) - click_element(:transfer_button) + click_element(:confirm_danger_button) fill_confirmation_text(project_name) confirm_transfer end diff --git a/qa/qa/page/project/settings/integrations.rb b/qa/qa/page/project/settings/integrations.rb index 0d5515aacdf..58b3badbb22 100644 --- a/qa/qa/page/project/settings/integrations.rb +++ b/qa/qa/page/project/settings/integrations.rb @@ -6,6 +6,7 @@ module QA module Settings class Integrations < QA::Page::Base view 'app/assets/javascripts/integrations/index/components/integrations_table.vue' do + element :jenkins_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern element :prometheus_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern element :jira_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern element :pipelines_email_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern @@ -22,10 +23,12 @@ module QA def click_jira_link click_element :jira_link end + + def click_jenkins_ci_link + click_element :jenkins_link + end end end end end end - -QA::Page::Project::Settings::Integrations.prepend_mod_with('Page::Project::Settings::Integrations', namespace: QA) diff --git a/qa/qa/page/project/settings/pages.rb b/qa/qa/page/project/settings/pages.rb new file mode 100644 index 00000000000..1417f7ec1b5 --- /dev/null +++ b/qa/qa/page/project/settings/pages.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class Pages < Page::Base + include QA::Page::Settings::Common + + view 'app/views/projects/pages/_access.html.haml' do + element :access_page_container + end + + def go_to_access_page + within_element(:access_page_container) do + find('a').click + end + end + end + end + end + end +end + +QA::Page::Project::Settings::Pages.prepend_mod_with("Page::Project::Settings::Pages", namespace: QA) diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb index 308cf6366a7..35fc87f717c 100644 --- a/qa/qa/page/project/settings/protected_branches.rb +++ b/qa/qa/page/project/settings/protected_branches.rb @@ -17,10 +17,6 @@ module QA element :allowed_to_merge_dropdown end - view 'app/views/shared/projects/protected_branches/_update_protected_branch.html.haml' do - element :allowed_to_merge - end - view 'app/views/projects/protected_branches/shared/_branches_list.html.haml' do element :protected_branches_list end diff --git a/qa/qa/page/project/settings/services/jenkins.rb b/qa/qa/page/project/settings/services/jenkins.rb index 8e092371491..39403995ce8 100644 --- a/qa/qa/page/project/settings/services/jenkins.rb +++ b/qa/qa/page/project/settings/services/jenkins.rb @@ -17,11 +17,11 @@ module QA element :save_changes_button end - def setup_service_with(jenkins_url:, project_name:) + def setup_service_with(jenkins_url:, project_name:, username:, password:) set_jenkins_url(jenkins_url) set_project_name(project_name) - set_username('admin') - set_password('password') + set_username(username) + set_password(password) click_save_changes_button end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 022e08215be..e048afee8b3 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -67,8 +67,8 @@ module QA end view 'app/views/shared/_ref_switcher.html.haml' do - element :branches_select element :branches_dropdown + element :branches_dropdown_content end view 'app/views/projects/blob/viewers/_loading.html.haml' do @@ -176,9 +176,9 @@ module QA end def switch_to_branch(branch_name) - find_element(:branches_select).click + find_element(:branches_dropdown).click - within_element(:branches_dropdown) do + within_element(:branches_dropdown_content) do click_on branch_name end end diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb index f35d27e658d..53a5eaf60c5 100644 --- a/qa/qa/page/project/sub_menus/settings.rb +++ b/qa/qa/page/project/sub_menus/settings.rb @@ -69,6 +69,14 @@ module QA end end + def go_to_pages_settings + hover_settings do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Pages') + 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 bc6c839bfe2..e572569e496 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -24,11 +24,11 @@ module QA end view 'app/assets/javascripts/ide/components/ide_tree_list.vue' do - element :file_list + element :file_list_container end view 'app/assets/javascripts/ide/components/file_templates/bar.vue' do - element :file_templates_bar + element :file_templates_container element :file_template_dropdown end @@ -110,7 +110,7 @@ module QA end def has_file?(file_name) - within_element(:file_list) do + within_element(:file_list_container) do has_element?(:file_name_content, file_name: file_name) end end @@ -173,7 +173,7 @@ module QA has_no_element?(:new_file_modal) wait_until(reload: false) do - within_element(:file_templates_bar) do + within_element(:file_templates_container) do click_element :file_template_dropdown fill_element :dropdown_filter_input, template @@ -291,12 +291,12 @@ module QA click_element(:fork_project_button) # wait for the fork to be created wait_until(reload: true) do - has_element?(:file_list) + has_element?(:file_list_container) end end def upload_file(file_path) - within_element(:file_list) do + within_element(:file_list_container) do find_element(:file_upload_field, visible: false).send_keys(file_path) end end diff --git a/qa/qa/page/view.rb b/qa/qa/page/view.rb index fa17b8fe302..6a6396b8ed4 100644 --- a/qa/qa/page/view.rb +++ b/qa/qa/page/view.rb @@ -37,7 +37,7 @@ module QA def self.evaluate(&block) Page::View::DSL.new.tap do |evaluator| - evaluator.instance_exec(&block) if block_given? + evaluator.instance_exec(&block) if block end end diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index 667dbc03fc3..d1cfdfbc16c 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -74,6 +74,13 @@ module QA response.code == HTTP_STATUS_OK end + # Parameters included in the query URL + # + # @return [Hash] + def query_parameters + @query_parameters ||= {} + end + private def resource_web_url(resource) @@ -87,7 +94,8 @@ module QA end def api_get_from(get_path) - request = Runtime::API::Request.new(api_client, get_path) + path = "#{get_path}#{query_parameters_to_string}" + request = Runtime::API::Request.new(api_client, path) response = get(request.url) if response.code == HTTP_STATUS_SERVER_ERROR @@ -101,6 +109,15 @@ module QA response end + # Query parameters formatted as `?key1=value1&key2=value2...` + # + # @return [String] + def query_parameters_to_string + query_parameters.each_with_object([]) do |(k, v), arr| + arr << "#{k}=#{v}" + end.join('&').prepend('?').chomp('?') # prepend `?` unless the string is blank + end + def api_post process_api_response(api_post_to(api_post_path, api_post_body)) end diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb index c3da9d47de5..60f6cbdfc51 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -38,10 +38,8 @@ module QA group_show.go_to_new_subgroup Page::Group::New.perform do |group_new| - group_new.click_create_group group_new.set_path(path) - group_new.set_visibility('Public') - group_new.create + group_new.create_subgroup end # Ensure that the group was actually created @@ -63,6 +61,13 @@ module QA "/groups/#{CGI.escape(determine_full_path)}" end + # Parameters included in the query URL + # + # @return [Hash] + def query_parameters + super.merge({ with_projects: false }) + end + def api_post_body { parent_id: sandbox.id, diff --git a/qa/qa/resource/group_ci_variable.rb b/qa/qa/resource/group_ci_variable.rb new file mode 100644 index 00000000000..f78d11b6c11 --- /dev/null +++ b/qa/qa/resource/group_ci_variable.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module QA + module Resource + class GroupCiVariable < Base + attr_accessor :key, :value, :masked, :protected + + attribute :group do + QA::Resource::Group.fabricate_via_api! + end + + def initialize + @masked = false + @protected = false + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def resource_web_url(resource) + super + rescue ResourceURLMissingError + # this particular resource does not expose a web_url property + end + + def api_get_path + "/groups/#{group.id}/variables/#{key}" + end + + def api_post_path + "/groups/#{group.id}/variables" + end + + def api_post_body + { + key: key, + value: value, + masked: masked, + protected: protected + } + end + end + end +end diff --git a/qa/qa/resource/impersonation_token.rb b/qa/qa/resource/impersonation_token.rb new file mode 100644 index 00000000000..3bd356b5e9b --- /dev/null +++ b/qa/qa/resource/impersonation_token.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +module QA + module Resource + class ImpersonationToken < Base + attr_writer :name + + attribute :id + attribute :user + attribute :token + attribute :expires_at + + def api_get_path + "/users/#{user.id}/impersonation_tokens/#{id}" + rescue NoValueError + token = parse_body(api_get_from("/users/#{user.id}/impersonation_tokens")).find { |t| t[:name] == name } + + raise ResourceNotFoundError unless token + + @id = token[:id] + retry + end + + def api_post_path + api_get_path + end + + def name + @name ||= "api-impersonation-access-token-#{Faker::Alphanumeric.alphanumeric(number: 8)}" + end + + def api_post_body + { + name: name, + scopes: ["api"], + expires_at: expires_at.to_s + } + end + + def api_delete_path + "/users/#{user.id}/impersonation_tokens/#{id}" + end + + def resource_web_url(resource) + super + rescue ResourceURLMissingError + # this particular resource does not expose a web_url property + end + + def revoke_via_browser_ui! + Flow::Login.sign_in_unless_signed_in(user: Runtime::User.admin) + + Page::Main::Menu.perform(&:go_to_admin_area) + Page::Admin::Menu.perform(&:go_to_users_overview) + Page::Admin::Overview::Users::Index.perform do |index| + index.search_user(user.username) + index.click_user(user.name) + end + + Page::Admin::Overview::Users::Show.perform do |show| + show.go_to_impersonation_tokens do |impersonation_tokens| + impersonation_tokens.revoke_first_token_with_name(name) + end + end + yield if block_given? + 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(user: Runtime::User.admin) + + Page::Main::Menu.perform(&:go_to_admin_area) + Page::Admin::Menu.perform(&:go_to_users_overview) + Page::Admin::Overview::Users::Index.perform do |index| + index.search_user(user.username) + index.click_user(user.name) + end + + Page::Admin::Overview::Users::Show.perform do |show| + show.go_to_impersonation_tokens do |impersonation_tokens| + impersonation_tokens.fill_token_name(name) + impersonation_tokens.check_api + impersonation_tokens.fill_expiry_date(expires_at) + impersonation_tokens.click_create_token_button + self.token = impersonation_tokens.created_access_token + end + end + + reload! + end + end + end +end diff --git a/qa/qa/resource/integrations/project.rb b/qa/qa/resource/integrations/project.rb new file mode 100644 index 00000000000..11e59408e22 --- /dev/null +++ b/qa/qa/resource/integrations/project.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module Resource + module Integrations + module Project + def find_integration(slug) + fetch_integrations.find do |integration| + integration[:slug] == slug + end + end + + def fetch_integrations + parse_body api_get_from(api_get_integrations) + end + + private + + def api_get_integrations + "#{api_get_path}/integrations" + end + end + end + end +end diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb index a12c210ea0e..ad0f183c603 100644 --- a/qa/qa/resource/personal_access_token.rb +++ b/qa/qa/resource/personal_access_token.rb @@ -17,20 +17,17 @@ module QA # If Runtime::Env.admin_personal_access_token is provided, fabricate via the API, # else, fabricate via the browser. def fabricate_via_api! - QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username).tap do |cached_token| - @token = cached_token if cached_token - end - return if @token + return if find_and_set_value resource = if Runtime::Env.admin_personal_access_token && !@user.nil? self.api_client = Runtime::API::Client.as_admin - super else fabricate! end - QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) if @user + self.token = api_response[:token] unless api_response.nil? + cache_token resource end @@ -60,7 +57,17 @@ module QA # this particular resource does not expose a web_url property end + def find_and_set_value + @token ||= QA::Resource::PersonalAccessTokenCache.get_token_for_username(user.username) + end + + def cache_token + QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, self.token) if @user && self.token + end + def fabricate! + return if find_and_set_value + Flow::Login.sign_in_unless_signed_in(user: user) Page::Main::Menu.perform(&:click_edit_profile_link) @@ -74,6 +81,10 @@ module QA token_page.click_create_token_button self.token = Page::Profile::PersonalAccessTokens.perform(&:created_access_token) + + cache_token + + self.token end end end diff --git a/qa/qa/resource/personal_access_token_cache.rb b/qa/qa/resource/personal_access_token_cache.rb index 3e9dc3fd7df..0874618201a 100644 --- a/qa/qa/resource/personal_access_token_cache.rb +++ b/qa/qa/resource/personal_access_token_cache.rb @@ -6,7 +6,17 @@ module QA @personal_access_tokens = {} def self.get_token_for_username(username) - @personal_access_tokens[username] + token = @personal_access_tokens[username] + + log_message = if token + %Q[Retrieved cached token for username: #{username}, last six chars of token:#{token[-6..]}] + else + %Q[No cached token found for username: #{username}] + end + + QA::Runtime::Logger.info(log_message) + + token end def self.set_token_for_username(username, token) diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 825041cbead..13c6f285259 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -4,6 +4,7 @@ module QA module Resource class Project < Base include Events::Project + include Integrations::Project include Members include Visibility @@ -20,13 +21,13 @@ module QA :name, :path, :add_name_uuid, - :description, :runners_token, :visibility, :template_name, :import, :import_status, - :import_error + :import_error, + :description attribute :group do Group.fabricate! do |group| @@ -107,7 +108,7 @@ module QA end new_page.choose_name(@name) - new_page.add_description(@description) + new_page.add_description(@description) if @description new_page.set_visibility(@visibility) new_page.disable_initialize_with_sast new_page.disable_initialize_with_readme unless @initialize_with_readme @@ -294,6 +295,21 @@ module QA ) end + def change_path(new_path) + response = put(request_url(api_put_path), path: new_path) + + unless response.code == HTTP_STATUS_OK + raise( + ResourceUpdateFailedError, + "Failed to update the project path to '#{new_path}'. Request returned (#{response.code}): `#{response}`." + ) + end + + # We need to manually set the path_with_namespace as reload! relies on it being correct and avoid 404s + result = parse_body(response) + @path_with_namespace = result[:path_with_namespace] + end + def default_branch reload!.api_response[:default_branch] || Runtime::Env.default_branch end @@ -458,10 +474,12 @@ module QA response = post(request_url(api_housekeeping_path), nil) - unless response.code == HTTP_STATUS_CREATED - raise ResourceQueryError, - "Could not perform housekeeping. Request returned (#{response.code}): `#{response.body}`." - end + return if response.code == HTTP_STATUS_CREATED + + raise( + ResourceQueryError, + "Could not perform housekeeping. Request returned (#{response.code}): `#{response.body}`." + ) end # Gets project statistics. diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb index 8e7527bccd4..12ecce1ce9a 100644 --- a/qa/qa/resource/sandbox.rb +++ b/qa/qa/resource/sandbox.rb @@ -57,6 +57,13 @@ module QA "/groups/#{path}" end + # Parameters included in the query URL + # + # @return [Hash] + def query_parameters + super.merge({ with_projects: false }) + end + def api_post_body { path: path, diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index f7aca2571c9..1fd097d0acf 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -291,6 +291,14 @@ module QA ENV['JIRA_HOSTNAME'] end + def jenkins_admin_username + ENV.fetch('QA_JENKINS_USER', 'administrator') + end + + def jenkins_admin_password + ENV.fetch('QA_JENKINS_PASS', 'password') + end + # this is set by the integrations job # which will allow bidirectional communication # between the app and the specs container @@ -484,6 +492,10 @@ module QA ENV.fetch('MAX_CAPYBARA_WAIT_TIME', 10).to_i end + def use_public_ip_api? + enabled?(ENV['QA_USE_PUBLIC_IP_API'], default: false) + end + private def remote_grid_credentials diff --git a/qa/qa/runtime/ip_address.rb b/qa/qa/runtime/ip_address.rb index 657dc789cff..fcb6db750bb 100644 --- a/qa/qa/runtime/ip_address.rb +++ b/qa/qa/runtime/ip_address.rb @@ -13,7 +13,10 @@ module QA def fetch_current_ip_address # When running on CI against a live environment such as staging.gitlab.com, # we use the public facing IP address - ip_address = if Env.running_in_ci? && !URI.parse(Scenario.gitlab_address).host.include?('.test') + non_test_host = !URI.parse(Scenario.gitlab_address).host.include?('.test') + has_no_public_ip = Env.running_in_ci? || Env.use_public_ip_api? + + ip_address = if has_no_public_ip && non_test_host response = get(PUBLIC_IP_ADDRESS_API) raise HostUnreachableError, "#{PUBLIC_IP_ADDRESS_API} is unreachable" unless response.code == Support::API::HTTP_STATUS_OK diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb index 0af42470a7c..e4eeca2000f 100644 --- a/qa/qa/runtime/user.rb +++ b/qa/qa/runtime/user.rb @@ -55,3 +55,5 @@ module QA end end end + +QA::Runtime::User.extend_mod_with('Runtime::User', namespace: QA) diff --git a/qa/qa/scenario/test/integration/metrics.rb b/qa/qa/scenario/test/integration/metrics.rb new file mode 100644 index 00000000000..c1297038a0f --- /dev/null +++ b/qa/qa/scenario/test/integration/metrics.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class Metrics < Test::Instance::All + tags :metrics + end + end + end + end +end diff --git a/qa/qa/scenario/test/sanity/selectors.rb b/qa/qa/scenario/test/sanity/selectors.rb index 8b355c5faf6..688fede1b2e 100644 --- a/qa/qa/scenario/test/sanity/selectors.rb +++ b/qa/qa/scenario/test/sanity/selectors.rb @@ -19,7 +19,7 @@ module QA validators.flat_map(&:errors).tap do |errors| break if errors.none? - warn <<~EOS + warn <<~WARN GitLab QA sanity selectors validation test detected problems with your merge request! @@ -42,15 +42,14 @@ module QA contribute, please open an issue in GitLab QA issue tracker. Please see errors described below. - - EOS + WARN warn errors end validators.each(&:validate!) - puts 'Views / selectors validation passed!' + QA::Runtime::Logger.info('Views / selectors validation passed!') end end end diff --git a/qa/qa/service/docker_run/gitlab_runner.rb b/qa/qa/service/docker_run/gitlab_runner.rb index 1584b577af1..45ab4ceff99 100644 --- a/qa/qa/service/docker_run/gitlab_runner.rb +++ b/qa/qa/service/docker_run/gitlab_runner.rb @@ -36,13 +36,14 @@ module QA end def register! - shell <<~CMD.tr("\n", ' ') + cmd = <<~CMD.tr("\n", ' ') docker run -d --rm --network #{runner_network} --name #{@name} #{'-v /var/run/docker.sock:/var/run/docker.sock' if @executor == :docker} --privileged #{@image} #{add_gitlab_tls_cert if @address.include? 'https'} && docker exec --detach #{@name} sh -c "#{register_command}" CMD + shell(cmd, mask_secrets: [@token]) wait_until_running_and_configured diff --git a/qa/qa/service/docker_run/jenkins.rb b/qa/qa/service/docker_run/jenkins.rb index 808fab80c63..88b69210382 100644 --- a/qa/qa/service/docker_run/jenkins.rb +++ b/qa/qa/service/docker_run/jenkins.rb @@ -4,11 +4,27 @@ module QA module Service module DockerRun class Jenkins < Base + include Mixins::ThirdPartyDocker + + attr_reader :port + def initialize - @image = 'registry.gitlab.com/gitlab-org/gitlab-qa/jenkins-gitlab:version1' + @image = "#{third_party_repository}/jenkins:latest" @name = 'jenkins-server' @port = '8080' - super() + super + end + + def network + @network || 'test' + end + + def username + Runtime::Env.jenkins_admin_username + end + + def password + Runtime::Env.jenkins_admin_password end def host_address @@ -16,30 +32,37 @@ module QA end def host_name - if !QA::Runtime::Env.running_in_ci? && !runner_network.equal?('airgapped') - 'localhost' - end + return 'localhost' unless QA::Runtime::Env.running_in_ci? super end def register! + authenticate_third_party + command = <<~CMD.tr("\n", ' ') docker run -d --rm --network #{network} --hostname #{host_name} --name #{@name} - --env JENKINS_HOME=jenkins_home + --env JENKINS_USER=#{username} + --env JENKINS_PASS=#{password} --publish #{@port}:8080 --publish 50000:50000 #{@image} CMD - if !QA::Runtime::Env.running_in_ci? && !runner_network.equal?('airgapped') - command.gsub!("--network #{network} ", '') - end + shell(command, mask_secrets: [password]) - shell command + wait_for_running + end + + private + + def wait_for_running + Support::Waiter.wait_until(max_duration: 10, reload_page: false, raise_on_failure: false) do + running? + end end end end diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index 8563c3656a8..c332e7a6198 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -30,7 +30,7 @@ module QA wait_until_shell_command_matches(dataloss_command, /Outdated repositories/) end - def replicated?(project_id) + def replicated?(project_id, project_name_prefix = 'gitaly_cluster') Support::Retrier.retry_until(raise_on_failure: false) do replicas = wait_until_shell_command(%(docker exec #{@gitlab} bash -c 'gitlab-rake "gitlab:praefect:replicas[#{project_id}]"')) do |line| QA::Runtime::Logger.debug(line.chomp) @@ -40,7 +40,7 @@ module QA # ---------------------------------------------------------------------------------------------------------------------------------------------------------------- # gitaly_cluster-3aff1f2bd14e6c98 | 23c4422629234d62b62adacafd0a33a8364e8619 | 23c4422629234d62b62adacafd0a33a8364e8619 | 23c4422629234d62b62adacafd0a33a8364e8619 # - break line if line.start_with?('gitaly_cluster') + break line if line.start_with?(project_name_prefix) break nil if line.include?('Something went wrong when getting replicas') end next false unless replicas @@ -101,7 +101,7 @@ module QA wait_until_shell_command_matches( "docker inspect -f {{.State.Running}} #{name}", /true/, - sleep_interval: 3, + sleep_interval: 1, max_duration: 180, retry_on_exception: true ) @@ -425,7 +425,7 @@ module QA end def value_for_node(data, node) - data.find(-> {{ value: 0 }}) { |item| item[:node] == node }[:value] + data.find(-> { { value: 0 } }) { |item| item[:node] == node }[:value] end def wait_for_replication(project_id) diff --git a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb index 79509bdbe01..df34bf32421 100644 --- a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb @@ -65,10 +65,8 @@ module QA end def verify_repository_import - expect(imported_project.api_response).to include( - description: 'Project for github import test', - import_error: nil - ) + expect(imported_project.reload!.description).to eq('Project for github import test') + expect(imported_project.api_response[:import_error]).to be_nil end def verify_commits_import diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb index c47afbd23f0..de460a39ccf 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 @@ -8,9 +8,14 @@ module QA describe 'Project import' do let(:logger) { Runtime::Logger.logger } let(:differ) { RSpec::Support::Differ.new(color: true) } + let(:gitlab_address) { QA::Runtime::Scenario.gitlab_address } + let(:dummy_url) { "https://example.com" } let(:created_by_pattern) { /\*Created by: \S+\*\n\n/ } let(:suggestion_pattern) { /suggestion:-\d+\+\d+/ } + let(:gh_link_pattern) { %r{https://github.com/#{github_repo}/(issues|pull)} } + let(:gl_link_pattern) { %r{#{gitlab_address}/#{imported_project.path_with_namespace}/-/(issues|merge_requests)} } + let(:event_pattern) { %r{(un)?assigned( to)? @\S+|mentioned in (issue|merge request) [!#]\d+|changed title from \*\*.*\*\* to \*\*.*\*\*} } # rubocop:disable Layout/LineLength let(:api_client) { Runtime::API::Client.as_admin } @@ -83,14 +88,14 @@ 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 # use base html url as key + hash[c.html_url.gsub(/\#\S+/, "")] << c.body&.gsub(gh_link_pattern, dummy_url) # use base html url as key 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 # use base html url as key + hash[c.html_url.gsub(/\#\S+/, "")] << c.body&.gsub(gh_link_pattern, dummy_url) # use base html url as key end end @@ -135,7 +140,7 @@ module QA target: { name: "GitLab", project_name: imported_project.path_with_namespace, - address: QA::Runtime::Scenario.gitlab_address, + address: gitlab_address, data: { branches: gl_branches.length, commits: gl_commits.length, @@ -381,15 +386,16 @@ module QA end logger.debug("Fetching comments for mr '#{mr[:title]}'") + comments = resource + .comments(auto_paginate: true, attempts: 2) + .reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) } + [mr[:iid], { url: mr[:web_url], title: mr[:title], body: sanitize_description(mr[:description]) || '', - comments: resource - .comments(auto_paginate: true, attempts: 2) - # remove system notes - .reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) } - .map { |c| sanitize_comment(c[:body]) } + events: events(comments), + comments: non_event_comments(comments) }] end.to_h end @@ -412,24 +418,54 @@ module QA end logger.debug("Fetching comments for issue '#{issue[:title]}'") + comments = resource.comments(auto_paginate: true, attempts: 2) + [issue[:iid], { url: issue[:web_url], title: issue[:title], body: sanitize_description(issue[:description]) || '', - comments: resource - .comments(auto_paginate: true, attempts: 2) - .map { |c| sanitize_comment(c[:body]) } + events: events(comments), + comments: non_event_comments(comments) }] end.to_h end end - # Remove added prefixes and legacy diff format from comments + # Fetch comments without events + # + # @param [Array] comments + # @return [Array] + def non_event_comments(comments) + comments + .reject { |c| c[:body].match?(event_pattern) } + .map { |c| sanitize_comment(c[:body]) } + end + + # Events + # + # @param [Array] comments + # @return [Array] + def events(comments) + comments + .select { |c| c[:body].match?(event_pattern) } + .map { |c| c[:body] } + end + + # Normalize comments and make them directly comparable + # + # * remove created by prefixes + # * unify suggestion format + # * replace github and gitlab urls - some of the links to objects get transformed to gitlab entities, some don't, + # update all links to example.com for now # # @param [String] body # @return [String] def sanitize_comment(body) - body.gsub(created_by_pattern, "").gsub(suggestion_pattern, "suggestion\r") + body + .gsub(created_by_pattern, "") + .gsub(suggestion_pattern, "suggestion\r") + .gsub(gl_link_pattern, dummy_url) + .gsub(gh_link_pattern, dummy_url) end # Remove created by prefix from descripion diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb index fc221c963b1..874626e01f1 100644 --- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb +++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage', :requires_admin, :skip_live_env, except: { job: 'review-qa-*' } do - describe 'rate limits' do + describe 'rate limits', :reliable do let(:rate_limited_user) { Resource::User.fabricate_via_api! } let(:api_client) { Runtime::API::Client.new(:gitlab, user: rate_limited_user) } let!(:request) { Runtime::API::Request.new(api_client, '/users') } 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 faef321c89d..444d86f63d3 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 @@ -36,6 +36,7 @@ module QA it( 'is allowed to push code to sub-group project via the CLI', + :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363345' ) do expect do @@ -52,6 +53,7 @@ module QA it( 'is allowed to create a file in sub-group project via the API', + :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363348' ) do expect do @@ -68,6 +70,7 @@ 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' ) do expect do @@ -116,6 +119,7 @@ module QA it( 'is not allowed to push code to parent group project via the CLI', + :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363344' ) do expect do @@ -148,6 +152,7 @@ module QA it( 'is not allowed to commit to parent group project via the API', + :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/363342' ) do expect do diff --git a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb index cd1b7730fa9..5ee6dfdb8d8 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb @@ -67,6 +67,28 @@ module QA it_behaves_like 'repository storage move' end + + # 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 + 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 + Resource::Project.fabricate_via_api! do |project| + project.name = 'repo-storage-move' + project.initialize_with_readme = true + project.repository_storage = source_storage[:name] + project.api_client = Runtime::API::Client.as_admin + end + end + + before do + praefect_manager.gitlab = 'gitlab-gitaly-cluster' + end + + it_behaves_like 'repository storage move' + 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 index 150378016e1..2b96c35415e 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb @@ -20,11 +20,6 @@ module QA praefect_manager.wait_for_replication(project.id) end - after do - # Leave the cluster in a suitable state for subsequent tests - praefect_manager.start_all_nodes - end - it 'reads from each node', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347833' do pre_read_data = praefect_manager.query_read_distribution @@ -42,14 +37,12 @@ module QA context 'when a node is unhealthy' do before do - praefect_manager.start_all_nodes praefect_manager.stop_secondary_node - praefect_manager.wait_for_secondary_node_health_check_failure end after do # Leave the cluster in a suitable state for subsequent tests - praefect_manager.start_all_nodes + 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 diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb index b6296b5a263..a53614960cd 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb @@ -3,10 +3,7 @@ require 'parallel' module QA - RSpec.describe 'Create', quarantine: { - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/361382', - type: :investigating - } do + RSpec.describe 'Create' do context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do let(:praefect_manager) { Service::PraefectManager.new } let(:project) do @@ -35,6 +32,9 @@ module QA # to `in_progress`, and create a job lock for each one. queue_size_target = 10 + # During normal operations we avoid create a replication event + # https://gitlab.com/groups/gitlab-org/-/epics/7741 + praefect_manager.stop_secondary_node Git::Repository.perform do |repository| repository.uri = project.repository_http_location.uri repository.use_default_credentials @@ -46,11 +46,12 @@ module QA end repository.push_all_branches end + praefect_manager.start_secondary_node - count = 0 - while count < 1 + Support::Retrier.retry_until(max_duration: 60) do count = praefect_manager.replication_queue_lock_count QA::Runtime::Logger.debug("Lock count: #{count}") + count >= 1 end praefect_manager.stop_praefect @@ -58,12 +59,12 @@ module QA praefect_manager.start_praefect - # Create a new project, push to it, and check that replication occurs - project_push = Resource::Repository::ProjectPush.fabricate! do |push| - push.project_name = "gitaly_cluster" + # Create a new project, and check that replication occurs + new_project = Resource::Project.fabricate! do |project| + project.initialize_with_readme = true end - expect(praefect_manager.replicated?(project_push.project.id)).to be true + expect(praefect_manager.replicated?(new_project.id, new_project.name)).to be true end end end diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb index e27f37abedf..47be7e0620b 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_repo_sync_spec.rb @@ -15,7 +15,6 @@ module QA end after do - praefect_manager.start_all_nodes praefect_manager.remove_repo_from_disk(repo1["relative_path"]) praefect_manager.remove_repo_from_disk(repo2["relative_path"]) praefect_manager.remove_repository_from_praefect_database(repo1["relative_path"]) 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 new file mode 100644 index 00000000000..98612d84b21 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/repository/tag_revision_trigger_prereceive_hook_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.initialize_with_readme = true + 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 + + 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 +end diff --git a/qa/qa/specs/features/api/5_package/container_registry_spec.rb b/qa/qa/specs/features/api/5_package/container_registry_spec.rb index 79384e374d7..8412c0b2872 100644 --- a/qa/qa/specs/features/api/5_package/container_registry_spec.rb +++ b/qa/qa/specs/features/api/5_package/container_registry_spec.rb @@ -3,7 +3,7 @@ require 'airborne' module QA - RSpec.describe 'Package', only: { subdomain: %i[staging pre] } do + RSpec.describe 'Package', :reliable, only: { subdomain: %i[staging staging-canary pre] } do include Support::API include Support::Helpers::MaskToken @@ -41,7 +41,7 @@ module QA stages: - build - test - + build: image: docker:19.03.12 stage: build @@ -60,7 +60,7 @@ module QA - docker build -t $IMAGE_TAG . - docker push $IMAGE_TAG - docker pull $IMAGE_TAG - + test: image: dwdraju/alpine-curl-jq:latest stage: test @@ -72,7 +72,7 @@ module QA - 'status_code=$(curl --request DELETE --head --output /dev/null --write-out "%{http_code}\n" --header "PRIVATE-TOKEN: #{masked_token}" "https://${CI_SERVER_HOST}/api/v4/projects/#{project.id}/registry/repositories/$id/tags/master")' - if [ $status_code -ne 200 ]; then exit 1; fi; - 'status_code=$(curl --head --output /dev/null --write-out "%{http_code}\n" --header "PRIVATE-TOKEN: #{masked_token}" "https://${CI_SERVER_HOST}/api/v4/projects/#{project.id}/registry/repositories/$id/tags/master")' - - if [ $status_code -ne 404 ]; then exit 1; fi; + - if [ $status_code -ne 404 ]; then exit 1; fi; YAML end diff --git a/qa/qa/specs/features/api/8_monitor/metrics_spec.rb b/qa/qa/specs/features/api/8_monitor/metrics_spec.rb new file mode 100644 index 00000000000..1235b996958 --- /dev/null +++ b/qa/qa/specs/features/api/8_monitor/metrics_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'GitLab Metrics', :aggregate_failures, :orchestrated, :metrics do + let(:web_uri) { URI.parse(Runtime::Scenario.gitlab_address) } + let(:endpoint) do + "#{web_uri.scheme}://#{web_uri.host}:#{port}#{path}" + end + + let(:response) { RestClient.get(endpoint) } + + describe 'Web metrics' do + describe 'via Rails controller endpoint' do + let(:port) { web_uri.port } + let(:path) { '/-/metrics' } + + it 'returns 200 OK and serves metrics', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/362911' do + # This does not currently work because it requires a special auth token to + # make an internal endpoint request. But we should probably test this, too. + skip + end + end + + describe 'via dedicated server' do + let(:port) { '8083' } + let(:path) { '/metrics' } + + it 'returns 200 OK and serves metrics', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/362912' do + expect(response.code).to be(200) + expect(response.body).to match(/^puma_/) + end + end + end + + describe 'Sidekiq metrics' do + describe 'via dedicated server' do + let(:port) { '8082' } + let(:path) { '/metrics' } + + it 'returns 200 OK and serves metrics', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/362913' do + expect(response.code).to be(200) + expect(response.body).to match(/^sidekiq_/) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/14_product_intelligence/performance_bar_spec.rb b/qa/qa/specs/features/browser_ui/14_analytics/performance_bar_spec.rb index 10076a329bc..867c54102ae 100644 --- a/qa/qa/specs/features/browser_ui/14_product_intelligence/performance_bar_spec.rb +++ b/qa/qa/specs/features/browser_ui/14_analytics/performance_bar_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Product Intelligence' do + RSpec.describe 'Analytics' do describe 'Performance bar display', :requires_admin, :skip_live_env do context 'when logged in as an admin user' do # performance metrics: pg, gitaly, redis, rugged (feature flagged), total (not always provided) diff --git a/qa/qa/specs/features/browser_ui/14_product_intelligence/service_ping_default_enabled_spec.rb b/qa/qa/specs/features/browser_ui/14_analytics/service_ping_default_enabled_spec.rb index cc2888063ca..7826aca3601 100644 --- a/qa/qa/specs/features/browser_ui/14_product_intelligence/service_ping_default_enabled_spec.rb +++ b/qa/qa/specs/features/browser_ui/14_analytics/service_ping_default_enabled_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Product Intelligence' do + RSpec.describe 'Analytics' do describe 'Service ping default enabled' do context 'when using default enabled from gitlab.yml config', :requires_admin, except: { job: 'review-qa-*' } do before do diff --git a/qa/qa/specs/features/browser_ui/14_product_intelligence/service_ping_disabled_spec.rb b/qa/qa/specs/features/browser_ui/14_analytics/service_ping_disabled_spec.rb index f762adf52a1..8b30d6a7ad7 100644 --- a/qa/qa/specs/features/browser_ui/14_product_intelligence/service_ping_disabled_spec.rb +++ b/qa/qa/specs/features/browser_ui/14_analytics/service_ping_disabled_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Product Intelligence' do + RSpec.describe 'Analytics' do describe 'Service ping disabled', :orchestrated, :service_ping_disabled, :requires_admin do context 'when disabled from gitlab.yml config' do before do diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb index db02d1e8390..2c331584cf7 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb @@ -9,7 +9,7 @@ module QA end end - let(:target_group) do + let!(:target_group) do Resource::Group.fabricate_via_api! do |group| group.path = "target-group-for-transfer_#{SecureRandom.hex(8)}" end @@ -19,48 +19,39 @@ module QA Resource::Project.fabricate_via_api! do |project| project.group = source_group project.name = 'transfer-project' - project.initialize_with_readme = true end end - let(:edited_readme_content) { 'Here is the edited content.' } + let(:readme_content) { 'Here is the edited content.' } before do - Flow::Login.sign_in - - project.visit! - - Page::Project::Show.perform do |project| - project.click_file('README.md') + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.add_files([ + { file_path: 'README.md', content: readme_content } + ]) end - Page::File::Show.perform(&:click_edit) + Flow::Login.sign_in - Page::File::Edit.perform do |file| - file.remove_content - file.add_content(edited_readme_content) - file.commit_changes - end + project.visit! end it 'user transfers a project between groups', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347878' do - # Retry is needed here as the target group is not avaliable for transfer right away. - QA::Support::Retrier.retry_on_exception(reload_page: page) do - Page::File::Show.perform(&:go_to_general_settings) + Page::File::Show.perform(&:go_to_general_settings) - Page::Project::Settings::Main.perform(&:expand_advanced_settings) + Page::Project::Settings::Main.perform(&:expand_advanced_settings) - Page::Project::Settings::Advanced.perform do |advanced| - advanced.transfer_project!(project.name, target_group.full_path) - end + Page::Project::Settings::Advanced.perform do |advanced| + advanced.transfer_project!(project.name, target_group.full_path) end Page::Project::Settings::Main.perform(&:click_project) Page::Project::Show.perform do |project| expect(project).to have_breadcrumb(target_group.path) - expect(project).to have_readme_content(edited_readme_content) + expect(project).to have_readme_content(readme_content) end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb index f85c07001e2..6c69e4c59d9 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Manage', :requires_admin, :skip_live_env do + context 'Manage', :reliable, :requires_admin, :skip_live_env do describe '2FA' do let!(:user) { Resource::User.fabricate_via_api! } let!(:user_api_client) { Runtime::API::Client.new(:gitlab, user: user) } diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb index 2c7656e20f1..1d30b915594 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/maintain_log_in_mixed_env_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', only: { subdomain: :staging }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/344213', type: :stale } do + RSpec.describe 'Manage', only: { subdomain: %i[staging staging-canary] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/344213', type: :stale } do describe 'basic user' do it 'remains logged in when redirected from canary to non-canary node', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347626' do Runtime::Browser.visit(:gitlab, Page::Main::Login) 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 85ab466078a..8cc772ed022 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 @@ -2,7 +2,7 @@ module QA RSpec.shared_examples 'registration and login' do - it 'allows the user to registers and login' do + it 'allows the user to register and login' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Resource::User.fabricate_via_browser_ui! @@ -39,7 +39,7 @@ module QA end end - describe 'standard', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347867' do + describe 'standard', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347867' do context 'when admin approval is not required' do before(:all) do set_require_admin_approval_after_user_signup_via_api(false) @@ -69,7 +69,7 @@ module QA Support::Waiter.wait_until(retry_on_exception: true, sleep_interval: 3) { !user.exists? } end - it 'allows recreating with same credentials', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347868' do + it 'allows recreating with same credentials', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347868' do expect(Page::Main::Menu.perform(&:signed_in?)).to be_falsy Flow::Login.sign_in(as: user, skip_page_validation: true) diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_badge_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_badge_spec.rb index 3921595204c..f624f2fb44f 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_badge_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_badge_spec.rb @@ -42,10 +42,6 @@ module QA expect(project.asset_exists?(expected_badge_image_url)).to be_truthy end end - - after do - project&.remove_via_api! - end end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index 0063ce2613a..d07fff80b19 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -10,7 +10,6 @@ module QA expect(project_page).to have_content( /Project \S?#{project_name}\S+ was successfully created/ ) - expect(project_page).to have_content('create awesome project test') expect(project_page).to have_content('The repository for this project is empty') end end @@ -26,7 +25,7 @@ module QA let(:project) do Resource::Project.fabricate_via_browser_ui! do |project| project.name = project_name - project.description = 'create awesome project test' + project.description = nil end end @@ -38,17 +37,13 @@ module QA let(:project) do Resource::Project.fabricate_via_browser_ui! do |project| project.name = project_name - project.description = 'create awesome project test' project.personal_namespace = Runtime::User.username + project.description = nil end end it_behaves_like 'successful project creation' end - - after do - project.remove_via_api! - end end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb index 8201e2772aa..dbfb114dc82 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Invite group' do + describe 'Invite group', :reliable do shared_examples 'invites group to project' do it 'verifies group is added and members can access project with correct access level' do Page::Project::Menu.perform(&:click_members) diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb index 2f148c4051c..29e590976d2 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Manage' do - describe 'Project owner permissions' do + describe 'Project owner permissions', :reliable do let!(:owner) do Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) end diff --git a/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb new file mode 100644 index 00000000000..5bcea1ff094 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/user/impersonation_token_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage' do + describe 'Impersonation tokens', :requires_admin do + let(:admin_api_client) { Runtime::API::Client.as_admin } + + let!(:user) do + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + usr.hard_delete_on_api_removal = true + end + end + + it( + 'can be created and revoked via the UI', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368888' + ) do + impersonation_token = QA::Resource::ImpersonationToken.fabricate_via_browser_ui! do |impersonation_token| + impersonation_token.api_client = admin_api_client + impersonation_token.user = user + end + + expect(impersonation_token.token).not_to be_nil + + impersonation_token.revoke_via_browser_ui! + + expect(page).to have_text("Revoked impersonation token #{impersonation_token.name}!") + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/design_management/add_design_content_spec.rb index 71415c4bb57..66208921f0e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/design_management/add_design_content_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', quarantine: { + RSpec.describe 'Plan', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/366839', type: :test_environment, only: { job: 'review-qa-*' } } do - context 'Design Management' do + describe 'Design Management' do let(:issue) { Resource::Issue.fabricate_via_api! } let(:design_filename) { 'banana_sample.gif' } let(:design) { File.absolute_path(File.join('qa', 'fixtures', 'designs', design_filename)) } @@ -16,7 +16,8 @@ module QA Flow::Login.sign_in end - it 'user adds a design and annotates it', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347822' do + it 'user adds a design and annotates it', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347822' do issue.visit! Page::Project::Issue::Show.perform do |issue| diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/design_management/archive_design_content_spec.rb index 9b969f563a2..8cbc6d7209c 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/design_management/archive_design_content_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', quarantine: { + RSpec.describe 'Plan', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/366839', type: :test_environment, only: { job: 'review-qa-*' } } do - context 'Design Management' do + describe 'Design Management' do let(:first_design) { Resource::Design.fabricate! } let(:second_design) do diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/design_management/modify_design_content_spec.rb index 6a0c51245ad..8f4902026d2 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/design_management/modify_design_content_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', quarantine: { + RSpec.describe 'Plan', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/366839', type: :test_environment, only: { job: 'review-qa-*' } } do - context 'Design Management' do + describe 'Design Management' do let(:design) do Resource::Design.fabricate_via_browser_ui! do |design| design.filename = 'testfile.png' 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 b8f1824126d..ed271533f87 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 @@ -1,11 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe( - 'Plan', - :smoke, - quarantine: { issue: 'https://gitlab.com/gitlab-com/gl-infra/production/-/issues/7099', type: :investigating, only: { subdomain: 'pre' } } - ) do + RSpec.describe 'Plan', :smoke do describe 'Issue creation' do let(:project) { Resource::Project.fabricate_via_api! } let(:closed_issue) { Resource::Issue.fabricate_via_api! { |issue| issue.project = project } } diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/custom_issue_template_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/custom_issue_template_spec.rb index 36b7378ee2a..206e6b8a456 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/custom_issue_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/custom_issue_template_spec.rb @@ -3,7 +3,7 @@ module QA RSpec.describe 'Plan', :reliable do describe 'Custom issue templates' do - let(:template_name) { 'custom_issue_template'} + let(:template_name) { 'custom_issue_template' } let(:template_content) { 'This is a custom issue template test' } let(:template_project) do diff --git a/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb b/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb index ea531d84634..4bfd253c992 100644 --- a/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/jenkins/jenkins_build_status_spec.rb @@ -1,8 +1,23 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', :requires_admin, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/195179', type: :flaky } do + RSpec.describe 'Create', :requires_admin, :skip_live_env, except: { job: 'review-qa-*' } do describe 'Jenkins integration' do + let(:jenkins_server) { Service::DockerRun::Jenkins.new } + + let(:jenkins_client) do + Vendor::Jenkins::Client.new( + jenkins_server.host_name, + port: jenkins_server.port, + user: Runtime::Env.jenkins_admin_username, + password: Runtime::Env.jenkins_admin_password + ) + end + + let(:jenkins_project_name) { "gitlab_jenkins_#{SecureRandom.hex(5)}" } + + let(:connection_name) { 'gitlab-connection' } + let(:project_name) { "project_with_jenkins_#{SecureRandom.hex(4)}" } let(:project) do @@ -13,97 +28,82 @@ module QA end end - before do - jenkins_server = run_jenkins_server + let(:access_token) do + Runtime::Env.personal_access_token ||= fabricate_access_token + end - Vendor::Jenkins::Page::Base.host = jenkins_server.host_address + before do + toggle_local_requests(true) + jenkins_server.register! - Runtime::Env.personal_access_token ||= fabricate_personal_access_token + Support::Waiter.wait_until(max_duration: 30, reload_page: false, retry_on_exception: true) do + jenkins_client.ready? + end - allow_requests_to_local_networks + configure_gitlab_jenkins + end - setup_jenkins + after do + jenkins_server&.remove! + toggle_local_requests(false) end it 'integrates and displays build status for MR pipeline in GitLab', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347788' do - login_to_gitlab + setup_project_integration - setup_project_integration_with_jenkins + jenkins_integration = project.find_integration('jenkins') + expect(jenkins_integration).not_to be(nil), 'Jenkins integration did not save' + expect(jenkins_integration[:active]).to be(true), 'Jenkins integration is not active' - expect(page).to have_text("Jenkins settings saved and active.") + job = create_jenkins_job - QA::Support::Retrier.retry_on_exception do - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.new_branch = false - push.file_name = "file_#{SecureRandom.hex(4)}.txt" - end - - Vendor::Jenkins::Page::LastJobConsole.perform do |job_console| - job_console.job_name = project_name + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.new_branch = false + push.file_name = "file_#{SecureRandom.hex(4)}.txt" + end - job_console.visit! + Support::Waiter.wait_until(max_duration: 60, raise_on_failure: false, reload_page: false) do + job.status == :success + end - Support::Waiter.wait_until(sleep_interval: 2, reload_page: page) do - job_console.has_successful_build? && job_console.no_failed_status_update? - end - end + expect(job.status).to eql(:success), "Build failed or is not found: #{job.log}" - project.visit! + project.visit! - Flow::Pipeline.visit_latest_pipeline + Flow::Pipeline.visit_latest_pipeline - Page::Project::Pipeline::Show.perform do |show| - expect(show).to have_build('jenkins', status: :success, wait: 15) - end + Page::Project::Pipeline::Show.perform do |show| + expect(show).to have_build('jenkins', status: :success, wait: 15) end end - after do - remove_jenkins_server - end + private - def setup_jenkins - Vendor::Jenkins::Page::Login.perform do |login_page| - login_page.visit! - login_page.login - end - - token_description = "token-#{SecureRandom.hex(8)}" - - Vendor::Jenkins::Page::NewCredentials.perform do |new_credentials| - new_credentials.visit_and_set_gitlab_api_token(Runtime::Env.personal_access_token, token_description) - end - - Vendor::Jenkins::Page::Configure.perform do |configure| - configure.visit_and_setup_gitlab_connection(patch_host_name(Runtime::Scenario.gitlab_address, 'gitlab'), token_description) do - configure.click_test_connection - expect(configure).to have_success - end - end + def setup_project_integration + login_to_gitlab - Vendor::Jenkins::Page::NewJob.perform do |new_job| - new_job.visit_and_create_new_job_with_name(project_name) - end + project.visit! - Vendor::Jenkins::Page::ConfigureJob.perform do |configure_job| - configure_job.job_name = project_name - configure_job.configure(scm_url: patch_host_name(project.repository_http_location.git_uri, 'gitlab')) - end - end + Page::Project::Menu.perform(&:click_project) + Page::Project::Menu.perform(&:go_to_integrations_settings) + Page::Project::Settings::Integrations.perform(&:click_jenkins_ci_link) - def run_jenkins_server - Service::DockerRun::Jenkins.new.tap do |runner| - runner.pull - runner.register! + QA::Page::Project::Settings::Services::Jenkins.perform do |jenkins| + jenkins.setup_service_with( + jenkins_url: patch_host_name(jenkins_server.host_address, 'jenkins-server'), + project_name: jenkins_project_name, + username: jenkins_server.username, + password: jenkins_server.password + ) end end - def remove_jenkins_server - Service::DockerRun::Jenkins.new.remove! + def login_to_gitlab + Flow::Login.sign_in end - def fabricate_personal_access_token + def fabricate_access_token login_to_gitlab token = Resource::PersonalAccessToken.fabricate!.token @@ -111,8 +111,23 @@ module QA token end - def login_to_gitlab - Flow::Login.sign_in + def create_jenkins_job + jenkins_client.create_job jenkins_project_name do |job| + job.gitlab_connection = connection_name + job.description = 'Just a job' + job.repo_url = patch_host_name(project.repository_http_location.git_uri, 'gitlab') + job.shell_command = 'sleep 5' + end + end + + def configure_gitlab_jenkins + jenkins_client.configure_gitlab_plugin( + patch_host_name(Runtime::Scenario.gitlab_address, 'gitlab'), + connection_name: connection_name, + access_token: access_token, + read_timeout: 20, + connection_timeout: 10 + ) end def patch_host_name(host_name, container_name) @@ -122,32 +137,8 @@ module QA host_name.gsub('localhost', ip_address) end - def setup_project_integration_with_jenkins - project.visit! - - Page::Project::Menu.perform(&:click_project) - Page::Project::Menu.perform(&:go_to_integrations_settings) - Page::Project::Settings::Integrations.perform(&:click_jenkins_ci_link) - - QA::Page::Project::Settings::Services::Jenkins.perform do |jenkins| - jenkins.setup_service_with(jenkins_url: patch_host_name(Vendor::Jenkins::Page::Base.host, 'jenkins-server'), - project_name: project_name) - end - end - - def allow_requests_to_local_networks - Page::Main::Menu.perform(&:sign_out_if_signed_in) - Flow::Login.sign_in_as_admin - Page::Main::Menu.perform(&:go_to_admin_area) - Page::Admin::Menu.perform(&:go_to_network_settings) - - Page::Admin::Settings::Network.perform do |network| - network.expand_outbound_requests do |outbound_requests| - outbound_requests.allow_requests_to_local_network_from_services - end - end - - Page::Main::Menu.perform(&:sign_out) + def toggle_local_requests(on) + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: on) end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_via_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_via_template_spec.rb index 3373f4f4233..6ce4217f8ac 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_via_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_via_template_spec.rb @@ -3,7 +3,7 @@ module QA RSpec.describe 'Create', :reliable do describe 'Merge request custom templates' do - let(:template_name) { 'custom_merge_request_template'} + let(:template_name) { 'custom_merge_request_template' } let(:template_content) { 'This is a custom merge request template test' } let(:template_project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb index d73fb57a581..aa637ac4d55 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/suggestions/custom_commit_suggestion_spec.rb @@ -52,7 +52,12 @@ module QA merge_request.click_commits_tab - expect(merge_request).to have_content(commit_message) + # Commit does not always display immediately and may require a page refresh + # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/368735 + # TODO: Remove page refresh logic once issue is resolved. + Support::Retrier.retry_on_exception(max_attempts: 2, reload_page: merge_request) do + expect(merge_request).to have_content(commit_message) + end end end end diff --git a/qa/qa/specs/features/browser_ui/6_release/pages/pages_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/3_create/pages/pages_pipeline_spec.rb index d1d2340e5f1..191c4a096e7 100644 --- a/qa/qa/specs/features/browser_ui/6_release/pages/pages_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/pages/pages_pipeline_spec.rb @@ -31,7 +31,8 @@ module QA pipeline.visit! end - it 'runs a Pages-specific pipeline', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347669' do + it('runs a Pages-specific pipeline', + 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) 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 8074e1fa992..3db8128bc6d 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 @@ -28,16 +28,16 @@ module QA context 'on a project with a commonly used LICENSE', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366842' do it_behaves_like 'project license detection' do - let(:license_file_name) {'bsd-3-clause'} - let(:rendered_license_name) {'BSD 3-Clause "New" or "Revised" License'} + let(:license_file_name) { 'bsd-3-clause' } + let(:rendered_license_name) { 'BSD 3-Clause "New" or "Revised" License' } end end context 'on a project with a less commonly used LICENSE', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366843' do it_behaves_like 'project license detection' do - let(:license_file_name) {'GFDL-1.2-only'} - let(:rendered_license_name) {'Other'} + let(:license_file_name) { 'GFDL-1.2-only' } + let(:rendered_license_name) { 'Other' } end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protected_tags_spec.rb index f6448fea2d4..fb87ca864f4 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protected_tags_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage' do + RSpec.describe 'Create' do describe 'Repository tags', :reliable do let(:project) do Resource::Project.fabricate_via_api! do |project| @@ -104,11 +104,8 @@ module QA end def add_members_to_project(project) - @developer_user = developer_user - @maintainer_user = maintainer_user - - project.add_member(@developer_user, Resource::Members::AccessLevel::DEVELOPER) - project.add_member(@maintainer_user, Resource::Members::AccessLevel::MAINTAINER) + project.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER) + project.add_member(maintainer_user, Resource::Members::AccessLevel::MAINTAINER) end end end 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 f374ecff3f2..55df1615f5c 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 @@ -3,7 +3,8 @@ module QA RSpec.describe 'Create' do describe 'SSH keys support', :smoke do - key_title = "key for ssh tests #{Time.now.to_f}" + let(:key_title) { "key for ssh tests #{Time.now.to_f}" } + key = nil before do diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb index b4519327a62..620e6870b26 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', :smoke, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326624', type: :investigating } do + RSpec.describe 'Create', :smoke do describe 'Personal snippet creation' do let(:snippet) do Resource::Snippet.fabricate_via_browser_ui! do |snippet| diff --git a/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb new file mode 100644 index 00000000000..f95f624d59a --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/source_editor/source_editor_toolbar_spec.rb @@ -0,0 +1,55 @@ +# frozen_string_literal: true +# tagged transient due to feature-flag caching flakiness. Remove tag along with feature flag removal. +module QA + RSpec.describe 'Create', feature_flag: { name: 'source_editor_toolbar', scope: :global } do + describe 'Source editor toolbar preview' do + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'empty-project-with-md' + project.initialize_with_readme = true + end + end + + let(:edited_readme_content) { 'Here is the edited content.' } + + before do + Runtime::Feature.enable(:source_editor_toolbar) + Flow::Login.sign_in + end + + after do + Runtime::Feature.disable(:source_editor_toolbar) + end + + it 'can preview markdown side-by-side while editing', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367749' do + project.visit! + Page::Project::Show.perform do |project| + project.click_file('README.md') + end + + Page::File::Show.perform(&:click_edit) + + # wait_until required due to feature_caching. Remove along with feature flag removal. + Page::File::Edit.perform do |file| + Support::Waiter.wait_until(sleep_interval: 2, max_duration: 60, reload_page: page, + retry_on_exception: true) do + expect(file).to have_element(:editor_toolbar_button) + end + file.remove_content + file.click_editor_toolbar + file.add_content('# ' + edited_readme_content) + file.wait_for_markdown_preview('h1', edited_readme_content) + file.commit_changes + end + + Page::File::Show.perform do |file| + aggregate_failures 'file details' do + expect(file).to have_notice('Your changes have been successfully committed.') + expect(file).to have_file_content(edited_readme_content) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb index 02ee94381b2..bef15b46fcd 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_tabs_spec.rb @@ -2,12 +2,7 @@ module QA RSpec.describe 'Verify' do - # TODO: Remove this test when feature flag is removed - # Flag rollout issue https://gitlab.com/gitlab-org/gitlab/-/issues/364257 - describe 'Pipeline editor', :reliable, feature_flag: { - name: :simulate_pipeline, - scope: :global - } do + describe 'Pipeline editor', :reliable do let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'pipeline-editor-project' @@ -42,19 +37,20 @@ module QA end before do - Runtime::Feature.disable(:simulate_pipeline) if Runtime::Feature.enabled?(:simulate_pipeline) + # Make sure a pipeline is created before visiting pipeline editor page. + # Otherwise, test might timeout before the page finishing fetching pipeline status. + Support::Waiter.wait_until { project.pipelines.present? } Flow::Login.sign_in project.visit! Page::Project::Menu.perform(&:go_to_pipeline_editor) end - after do - project&.remove_via_api! - end - context 'when CI has valid syntax' do - it 'shows valid validations', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349128' do + it( + 'shows valid validations', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368332' + ) do Page::Project::PipelineEditor::Show.perform do |show| aggregate_failures do expect(show.ci_syntax_validate_message).to have_content('Pipeline syntax is correct') @@ -65,8 +61,9 @@ module QA expect(show).to have_job(job), "Pipeline graph does not have job #{job}." end - show.go_to_lint_tab - expect(show.tab_alert_message).to have_content('Syntax is correct') + show.go_to_validate_tab + show.simulate_pipeline + expect(show.tab_alert_title).to have_content('Simulation completed successfully') show.go_to_view_merged_yaml_tab expect(show).to have_source_editor @@ -76,7 +73,10 @@ module QA end context 'when CI has invalid syntax' do - it 'shows invalid validations', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349129' do + it( + 'shows invalid validations', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368333' + ) do invalid_msg = 'syntax is invalid' Page::Project::PipelineEditor::Show.perform do |show| @@ -86,8 +86,9 @@ module QA show.go_to_visualize_tab expect(show.tab_alert_message).to have_content(invalid_msg) - show.go_to_lint_tab - expect(show.tab_alert_message).to have_content('Syntax is incorrect') + show.go_to_validate_tab + show.simulate_pipeline + expect(show.tab_alert_title).to have_content('Pipeline simulation completed with errors') show.go_to_view_merged_yaml_tab expect(show.tab_alert_message).to have_content(invalid_msg) 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 new file mode 100644 index 00000000000..412498476f0 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_with_image_pull_policy_spec.rb @@ -0,0 +1,175 @@ +# 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 + 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('-')}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'pipeline-with-image-pull-policy' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = runner_name + runner.tags = [runner_name] + runner.executor = :docker + end + 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 + project.visit! + Flow::Pipeline.visit_latest_pipeline + end + + after do + Runtime::Feature.disable(:ci_docker_image_pull_policy) + + runner.remove_via_api! + end + + context 'when policy is allowed' do + let(:allowed_policies) { %w[if-not-present always never] } + + where do + { + 'with [always] policy' => { + pull_policies: %w[always], + pull_image: true, + message: 'Pulling docker image ruby:2.6', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367154' + }, + 'with [always if-not-present] policies' => { + pull_policies: %w[always if-not-present], + pull_image: true, + message: 'Pulling docker image ruby:2.6', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368857' + }, + 'with [if-not-present] policy' => { + pull_policies: %w[if-not-present], + pull_image: true, + message: 'Using locally found image version due to "if-not-present" pull policy', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368858' + }, + 'with [never] policy' => { + pull_policies: %w[never], + pull_image: false, + message: 'Pulling docker image', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368859' + } + } + end + + with_them do + it 'applies pull policy in job correctly', testcase: params[:testcase] do + visit_job + + if pull_image + expect(job_log).to have_content(message), + "Expected to find #{message} in #{job_log}, but didn't." + else + expect(job_log).not_to have_content(message), + "Found #{message} in #{job_log}, but didn't expect to." + end + end + end + end + + context 'when policy is not allowed' do + let(:allowed_policies) { %w[never] } + let(:pull_policies) { %w[always] } + + let(:message) do + 'ERROR: Preparation failed: the configured PullPolicies ([always])'\ + ' are not allowed by AllowedPullPolicies ([never])' + end + + it( + 'fails job with policy not allowed message', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/368853' + ) do + visit_job + + expect(job_log).to have_content(message), + "Expected to find #{message} in #{job_log}, but didn't." + end + end + + private + + def update_runner_policy(allowed_policies) + Runtime::Logger.info('Updating runner config to allow pull policies...') + + # Copy config.toml file from docker to local + # Update local file with allowed_pull_policies config + # Copy file with new content back to docker + tempdir = Tempfile.new('config.toml') + QA::Service::Shellout.shell("docker cp #{runner_name}:/etc/gitlab-runner/config.toml #{tempdir.path}") + + File.open(tempdir.path, 'a') do |f| + f << %Q[ allowed_pull_policies = #{allowed_policies}\n] + end + + QA::Service::Shellout.shell("docker cp #{tempdir.path} #{runner_name}:/etc/gitlab-runner/config.toml") + + tempdir.close! + + # Give runner some time to pick up new configuration + sleep(30) + end + + def add_ci_file + 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 + default: + image: ruby:2.6 + tags: [#{runner_name}] + + #{job_name}: + script: echo "Using pull policies #{pull_policies}" + image: + name: ruby:2.6 + pull_policy: #{pull_policies} + YAML + } + ] + ) + end + end + + def visit_job + Page::Project::Pipeline::Show.perform do |show| + Support::Waiter.wait_until { show.completed? } + + show.click_job(job_name) + end + end + + def job_log + Page::Project::Job::Show.perform(&:output) + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb index 6918c087a4e..5a29b44e8b3 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Package' do - describe 'Container Registry', :reliable, only: { subdomain: %i[staging pre] } do + describe 'Container Registry', only: { subdomain: %i[staging staging-canary pre] } do let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'project-with-registry' @@ -37,7 +37,7 @@ module QA do docker info && break sleep 1s - done + done script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - docker build -t $IMAGE_TAG . diff --git a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb index d9d75a8ae7a..ad820977747 100644 --- a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb @@ -4,6 +4,7 @@ module QA RSpec.describe 'Package', :orchestrated, :registry, only: { pipeline: :main } do describe 'Dependency Proxy' do using RSpec::Parameterized::TableSyntax + include Support::Helpers::MaskToken let(:project) do Resource::Project.fabricate_via_api! do |project| @@ -21,6 +22,19 @@ module QA end end + let(:group_deploy_token) do + Resource::GroupDeployToken.fabricate_via_api! do |deploy_token| + deploy_token.name = 'dp-group-deploy-token' + deploy_token.group = project.group + deploy_token.scopes = %w[ + read_registry + write_registry + ] + end + end + + let(:personal_access_token) { Runtime::Env.personal_access_token } + let(:uri) { URI.parse(Runtime::Scenario.gitlab_address) } let(:gitlab_host_with_port) { "#{uri.host}:#{uri.port}" } let(:dependency_proxy_url) { "#{gitlab_host_with_port}/#{project.group.full_path}/dependency_proxy/containers" } @@ -43,12 +57,92 @@ module QA runner.remove_via_api! end - where(:case_name, :docker_client_version, :testcase) do - 'using docker:19.03.12' | 'docker:19.03.12' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347605' - 'using docker:20.10' | 'docker:20.10' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347604' + where do + { + 'using docker:18.09.9 and a personal access token' => { + docker_client_version: 'docker:18.09.9', + authentication_token_type: :personal_access_token, + token_name: 'Personal Access Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370195' + }, + 'using docker:18.09.9 and a group deploy token' => { + docker_client_version: 'docker:18.09.9', + authentication_token_type: :group_deploy_token, + token_name: 'Deploy Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370196' + }, + 'using docker:18.09.9 and a ci job token' => { + docker_client_version: 'docker:18.09.9', + authentication_token_type: :ci_job_token, + token_name: 'Job Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370198' + }, + 'using docker:19.03.12 and a personal access token' => { + docker_client_version: 'docker:19.03.12', + authentication_token_type: :personal_access_token, + token_name: 'Personal Access Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370189' + }, + 'using docker:19.03.12 and a group deploy token' => { + docker_client_version: 'docker:19.03.12', + authentication_token_type: :group_deploy_token, + token_name: 'Deploy Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370190' + }, + 'using docker:19.03.12 and a ci job token' => { + docker_client_version: 'docker:19.03.12', + authentication_token_type: :ci_job_token, + token_name: 'Job Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370191' + }, + 'using docker:20.10 and a personal access token' => { + docker_client_version: 'docker:20.10', + authentication_token_type: :personal_access_token, + token_name: 'Personal Access Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370192' + }, + 'using docker:20.10 and a group deploy token' => { + docker_client_version: 'docker:20.10', + authentication_token_type: :group_deploy_token, + token_name: 'Deploy Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370193' + }, + 'using docker:20.10 and a ci job token' => { + docker_client_version: 'docker:20.10', + authentication_token_type: :ci_job_token, + token_name: 'Job Token', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/370194' + } + } end with_them do + let(:auth_token) do + case authentication_token_type + when :personal_access_token + use_ci_variable(name: 'PERSONAL_ACCESS_TOKEN', value: personal_access_token, project: project) + when :group_deploy_token + use_group_ci_variable( + name: "GROUP_DEPLOY_TOKEN_#{group_deploy_token.id}", + value: group_deploy_token.token, + group: project.group + ) + when :ci_job_token + '$CI_JOB_TOKEN' + end + end + + let(:auth_user) do + case authentication_token_type + when :personal_access_token + "$CI_REGISTRY_USER" + when :group_deploy_token + "\"#{group_deploy_token.username}\"" + when :ci_job_token + 'gitlab-ci-token' + end + end + it "pulls an image using the dependency proxy", testcase: params[:testcase] do Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| @@ -65,8 +159,7 @@ module QA command: ["--insecure-registry=gitlab.test:80"] before_script: - apk add curl jq grep - - echo $CI_DEPENDENCY_PROXY_SERVER - - docker login -u "$CI_DEPENDENCY_PROXY_USER" -p "$CI_DEPENDENCY_PROXY_PASSWORD" gitlab.test:80 + - docker login -u #{auth_user} -p #{auth_token} gitlab.test:80 script: - docker pull #{dependency_proxy_url}/#{image_sha} - TOKEN=$(curl "https://auth.docker.io/token?service=registry.docker.io&scope=repository:ratelimitpreview/test:pull" | jq --raw-output .token) diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb index 980c6da2576..608dd7e089f 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Configure' do - describe 'AutoDevOps Templates', only: { subdomain: :staging } do + describe 'AutoDevOps Templates', only: { subdomain: %i[staging staging-canary] } do using RSpec::Parameterized::TableSyntax # specify jobs to be disabled in the pipeline. 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 dca6f961047..f1a2eb71390 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 @@ -2,7 +2,7 @@ module QA RSpec.describe 'Configure', - only: { subdomain: :staging }, + only: { subdomain: %i[staging staging-canary] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/1198', type: :waiting_on diff --git a/qa/qa/specs/features/sanity/feature_flags_spec.rb b/qa/qa/specs/features/sanity/feature_flags_spec.rb index f771978802e..acb9528fe6a 100644 --- a/qa/qa/specs/features/sanity/feature_flags_spec.rb +++ b/qa/qa/specs/features/sanity/feature_flags_spec.rb @@ -26,7 +26,7 @@ module QA end let(:flag) { Pathname.new(file.path).basename('.yml').to_s } - let(:root) { '..'} + let(:root) { '..' } before do definition = <<~YAML @@ -78,7 +78,7 @@ module QA end context 'with an EE feature flag' do - let(:root) { '../ee'} + let(:root) { '../ee' } include_examples 'gets flag value' end diff --git a/qa/qa/support/helpers/mask_token.rb b/qa/qa/support/helpers/mask_token.rb index 1f8161f7173..0c0af524c97 100644 --- a/qa/qa/support/helpers/mask_token.rb +++ b/qa/qa/support/helpers/mask_token.rb @@ -13,6 +13,16 @@ module QA end "$#{name}" end + + def use_group_ci_variable(name:, value:, group:) + Resource::GroupCiVariable.fabricate_via_api! do |ci_variable| + ci_variable.group = group + ci_variable.key = name + ci_variable.value = value + ci_variable.protected = true + end + "$#{name}" + end end end end diff --git a/qa/qa/support/knapsack_report.rb b/qa/qa/support/knapsack_report.rb index 8114e838ede..659b8f10e0a 100644 --- a/qa/qa/support/knapsack_report.rb +++ b/qa/qa/support/knapsack_report.rb @@ -98,7 +98,7 @@ module QA # # @return [void] def setup_logger! - Knapsack.logger = QA::Runtime::Logger.logger + Knapsack.logger = logger end # Set knapsack environment variables @@ -112,9 +112,9 @@ module QA # Logger instance # - # @return [Logger] + # @return [ActiveSupport::Logger] def logger - @logger ||= Knapsack.logger + QA::Runtime::Logger.logger end # GCS client diff --git a/qa/qa/support/loglinking.rb b/qa/qa/support/loglinking.rb index caf381912d3..ceddd35da17 100644 --- a/qa/qa/support/loglinking.rb +++ b/qa/qa/support/loglinking.rb @@ -8,18 +8,18 @@ module QA PRODUCTION_ADDRESS = 'https://gitlab.com' PRE_PROD_ADDRESS = 'https://pre.gitlab.com' SENTRY_ENVIRONMENTS = { - staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg', + staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg', staging_canary: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny', - staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref', - pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre', - canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd', - production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny' + staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref', + pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre', + canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd', + production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny' }.freeze KIBANA_ENVIRONMENTS = { - staging: 'https://nonprod-log.gitlab.net/', + staging: 'https://nonprod-log.gitlab.net/', staging_canary: 'https://nonprod-log.gitlab.net/', - canary: 'https://log.gprd.gitlab.net/', - production: 'https://log.gprd.gitlab.net/' + canary: 'https://log.gprd.gitlab.net/', + production: 'https://log.gprd.gitlab.net/' }.freeze def self.failure_metadata(correlation_id) diff --git a/qa/qa/support/parallel_pipeline_jobs.rb b/qa/qa/support/parallel_pipeline_jobs.rb new file mode 100644 index 00000000000..a551bf9978b --- /dev/null +++ b/qa/qa/support/parallel_pipeline_jobs.rb @@ -0,0 +1,76 @@ +# frozen_string_literal: true + +module QA + module Support + # Helper utility to fetch parallel job names in a given pipelines stage + # + class ParallelPipelineJobs + include API + + PARALLEL_JOB_NAME_PATTERN = %r{^\S+ \d+/\d+$}.freeze + + def initialize(stage_name:, project_id:, pipeline_id:, access_token:) + @stage_name = stage_name + @access_token = access_token + @project_id = project_id || raise("project_id must be provided") + @pipeline_id = pipeline_id || raise("pipeline_id must be provided") + end + + # Fetch parallel job names in given stage + # + # Default to arguments available on CI + # + # @param [String] stage_name + # @param [Integer] project_id + # @param [Integer] pipeline_id + # @param [String] access_token + # @return [Array] + def self.fetch( + stage_name:, + access_token:, + project_id: ENV["CI_PROJECT_ID"], + pipeline_id: ENV["CI_PIPELINE_ID"] + ) + new( + stage_name: stage_name, + project_id: project_id, + pipeline_id: pipeline_id, + access_token: access_token + ).parallel_jobs + end + + # Parallel job list + # + # @return [Array<String>] + def parallel_jobs + api_get("projects/#{project_id}/pipelines/#{pipeline_id}/jobs?per_page=100") + .select { |job| job[:stage] == stage_name && job[:name].match?(PARALLEL_JOB_NAME_PATTERN) } + .map { |job| job[:name].gsub(%r{ \d+/\d+}, "") } + .uniq + end + + private + + attr_reader :stage_name, :access_token, :project_id, :pipeline_id + + # Api get request + # + # @param [String] path + # @param [Hash] payload + # @return [Hash, Array] + def api_get(path) + response = get("#{api_url}/#{path}", { headers: { "PRIVATE-TOKEN" => access_token } }) + raise "Failed to fetch pipeline jobs: '#{response.body}'" unless response.code == API::HTTP_STATUS_OK + + parse_body(response) + end + + # Gitlab api url + # + # @return [String] + def api_url + @api_url ||= ENV['CI_API_V4_URL'] || "https://gitlab.com/api/v4" + end + end + end +end diff --git a/qa/qa/tools/delete_projects.rb b/qa/qa/tools/delete_projects.rb index 96ea5f8de7e..da46606f286 100644 --- a/qa/qa/tools/delete_projects.rb +++ b/qa/qa/tools/delete_projects.rb @@ -9,6 +9,7 @@ module QA module Tools class DeleteProjects include Support::API + include Lib::Project def initialize raise ArgumentError, "Please provide GITLAB_ADDRESS environment variable" unless ENV['GITLAB_ADDRESS'] @@ -30,25 +31,12 @@ module QA project_ids = fetch_project_ids(group_id, total_project_pages) $stdout.puts "Number of projects to be deleted: #{project_ids.length}" - delete_projects(project_ids) unless project_ids.empty? + delete_projects(project_ids, @api_client) unless project_ids.empty? $stdout.puts "\nDone" end private - def delete_projects(project_ids) - $stdout.puts "Deleting #{project_ids.length} projects..." - project_ids.each do |project_id| - request_url = Runtime::API::Request.new(@api_client, "/projects/#{project_id}").url - path = parse_body(get(request_url))[:path_with_namespace] - $stdout.puts "\nDeleting project #{path}..." - - delete_response = delete(request_url) - dot_or_f = delete_response.code.between?(200, 300) ? "\e[32m.\e[0m" : "\e[31mF - #{delete_response}\e[0m" - print dot_or_f - end - end - def fetch_group_id group_name = ENV['TOP_LEVEL_GROUP_NAME'] || "gitlab-qa-sandbox-group-#{Time.now.wday + 1}" group_search_response = get Runtime::API::Request.new(@api_client, "/groups/#{group_name}").url diff --git a/qa/qa/tools/delete_user_projects.rb b/qa/qa/tools/delete_user_projects.rb new file mode 100644 index 00000000000..9c031e352b4 --- /dev/null +++ b/qa/qa/tools/delete_user_projects.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +# This script deletes all projects owned by a given USER_ID in their personal namespace +# Required environment variables: USER_ID, GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS +# Run `rake delete_user_projects` + +module QA + module Tools + class DeleteUserProjects + include Support::API + include Lib::Project + + def initialize(delete_before: (Date.today - 1).to_s, dry_run: false) + unless ENV['GITLAB_ADDRESS'] + raise ArgumentError, "Please provide GITLAB_ADDRESS environment variable" + end + + unless ENV['GITLAB_QA_ACCESS_TOKEN'] + raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN environment variable" + end + + unless ENV['USER_ID'] + raise ArgumentError, "Please provide USER_ID environment variable" + end + + @delete_before = Date.parse(delete_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...' + + projects_head_response = head Runtime::API::Request.new(@api_client, "/users/#{ENV['USER_ID']}/projects", + per_page: "100").url + total_project_pages = projects_head_response.headers[:x_total_pages] + + $stdout.puts "Total project pages: #{total_project_pages}" + + project_ids = fetch_project_ids(total_project_pages) + + delete_projects(project_ids, @api_client, @dry_run) unless project_ids.empty? + $stdout.puts "\nDone" + end + + private + + def fetch_project_ids(total_project_pages) + projects_ids = [] + + total_project_pages.to_i.times do |page_no| + projects_response = get Runtime::API::Request.new(@api_client, "/users/#{ENV['USER_ID']}/projects", + page: (page_no + 1).to_s, per_page: "100").url + projects_ids.concat(JSON.parse(projects_response.body) + .select { |project| Date.parse(project["created_at"]) < @delete_before } + .map { |project| project["id"] }) + end + + projects_ids.uniq + end + end + end +end diff --git a/qa/qa/tools/lib/project.rb b/qa/qa/tools/lib/project.rb new file mode 100644 index 00000000000..52e5e5c63a4 --- /dev/null +++ b/qa/qa/tools/lib/project.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module QA + module Tools + module Lib + module Project + def delete_projects(project_ids, api_client, dry_run = false) + if dry_run + $stdout.puts "Following #{project_ids.length} projects would be deleted:" + else + $stdout.puts "Deleting #{project_ids.length} projects..." + end + + project_ids.each do |project_id| + request_url = Runtime::API::Request.new(api_client, "/projects/#{project_id}").url + parsed_body = parse_body(get(request_url)) + path = parsed_body[:path_with_namespace] + created_at = parsed_body[:created_at] + + if dry_run + $stdout.puts "#{path} - created at: #{created_at}" + else + $stdout.puts "\nDeleting project #{path} - created at: #{created_at}" + delete_response = delete(request_url) + dot_or_f = delete_response.code.between?(200, 300) ? "\e[32m.\e[0m" : "\e[31mF - #{delete_response}\e[0m" + print dot_or_f + end + end + end + end + end + end +end diff --git a/qa/qa/vendor/jenkins/README.md b/qa/qa/vendor/jenkins/README.md new file mode 100644 index 00000000000..98304bd7cb8 --- /dev/null +++ b/qa/qa/vendor/jenkins/README.md @@ -0,0 +1,61 @@ +## jenkins_client + +For use with [this jenkins docker image](https://gitlab.com/gitlab-org/quality/third-party-docker-images/jenkins) + +### usage + +instantiate client + +```ruby +client = Jenkins::Client.new( + '127.0.0.1', + user: ENV['JENKINS_ADMIN_USER'], + password: ENV['JENKINS_ADMIN_PASS'], +) +``` + +configure gitlab plugin and create a job + +```ruby +connection_name = 'gitlab-connection' + +client.configure_gitlab_plugin( + ENV['GITLAB_URL'], + connection_name: connection_name, + access_token: ENV['GITLAB_ACCESS_TOKEN'], + read_timeout: 20, + connection_timeout: 10 +) + +job = client.create_job 'Job Name' do |job| + job.gitlab_connection = connection_name + job.description = 'Job Description' + job.repo_url = 'https://location-of-project.git' + job.shell_command = 'sleep 20' +end +``` + +view info about the job + +```ruby +while job.running? + puts "Number of active builds: #{job.active_runs}" +end + +puts "Last build status #{job.status}" +puts "Last build log #{job.log}" +``` + +### developer notes + +This client makes extensive use of the [/script api](https://docs.cloudbees.com/docs/cloudbees-ci-kb/latest/client-and-managed-masters/execute-groovy-with-a-rest-call). + +Groovy is a dynamic language hosted on the Java platform. Please refer to https://learnxinyminutes.com/docs/groovy/ for basic syntax. + +The scripts may reference the code api of jenkins and the gitlab jenkins plugin. Here are some articles and source files to reference when debugging. + +* [Setting Jenkins Credentials](https://nickcharlton.net/posts/setting-jenkins-credentials-with-groovy.html) +* [GitLabConnectionConfig](https://github.com/jenkinsci/gitlab-plugin/blob/master/src/main/java/com/dabsquared/gitlabjenkins/connection/GitLabConnectionConfig.java) and [GitLabConnection](https://github.com/jenkinsci/gitlab-plugin/blob/master/src/main/java/com/dabsquared/gitlabjenkins/connection/GitLabConnection.java) +* [Jenkins.instance.getProjects](https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/jenkins/model/Jenkins.java#L1878) +* [Job.getBuilds](https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Job.java#L734) +* [Run.getResult](https://github.com/jenkinsci/jenkins/blob/master/core/src/main/java/hudson/model/Run.java#L491) diff --git a/qa/qa/vendor/jenkins/client.rb b/qa/qa/vendor/jenkins/client.rb new file mode 100644 index 00000000000..d7666255010 --- /dev/null +++ b/qa/qa/vendor/jenkins/client.rb @@ -0,0 +1,298 @@ +# frozen_string_literal: true + +require 'base64' +require 'cgi' +require 'fileutils' +require 'json' +require 'nokogiri' +require 'rest-client' +require 'securerandom' + +require_relative './helpers' +require_relative './job' + +module QA + module Vendor + module Jenkins + NetworkError = Class.new(StandardError) + NotParseableError = Class.new(StandardError) + class Client + include Helpers + + attr_accessor :cookies + + DEFAULT_SERVER_PORT = 8080 + + # @param host [String] the ip or hostname of the jenkins server + # @param user [String] the Jenkins admin user + # @param password [String] the Jenkins admin password + # @param port [Integer] the port that Jenkins is serving on + def initialize(host, user:, password:, port: nil) + @host = host + @user = user + @password = password + @port = port + @cookies = {} + end + + def ready? + !!try_parse(RestClient.get(crumb_path, auth_headers).body) + end + + # Creates a new job in Jenkins + # + # @param name [String] the name of the job + # @yieldparam job [Jenkins::Job] the job to be configured + # @return [Jenkins::Job] the created job in Jenkins + def create_job(name) + job = Job.new(name, self) + yield job if block_given? + job.create + job + end + + # Is a given job running? + # + # @param name [String] the name of the job + # @return [Boolean] is the job running? + def job_running?(name) + res = execute <<~GROOVY + project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{name}')} + build = project.getBuilds().find{b -> b.getExecutor()} + return build ? build.getExecutor().isActive() : false + GROOVY + JSON.parse parse_result(res) + end + + # Number of builds currently executing for a given job + # + # @param name [String] the name of the job + # @return [Integer] the number of builds currently running + def number_of_jobs_running(name) + res = execute <<~GROOVY + project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{name}')} + builds = project.getBuilds().findAll{b -> b.getExecutor()} + return builds.size + GROOVY + JSON.parse parse_result(res)&.to_i + end + + # Latest build status for a job + # + # @param name [String] the name of the job + # @return [Symbol] the latest build status eg, (:success, :failure, etc) + def last_build_status(name) + res = execute <<~GROOVY + project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{name}')} + build = project.getBuilds()[-1] + return build.getResult() + GROOVY + parse_result(res)&.downcase&.to_sym + end + + # Latest build id for a job + # Can be used to reference in other queries + # + # @param job_name [String] the name of the job + # @return [Integer] the latest build id + def last_build_id(job_name) + res = execute <<~GROOVY + project = Jenkins.instance.getProjects().find{p -> p.getName().equals('#{job_name}')} + build = project.getBuilds()[-1] + return build.getId() + GROOVY + parse_result(res)&.to_i + end + + # Latest build log for a job + # + # @param job_name [String] the name of the job + # @param start [Integer] the log offset to return + # @return [String] the latest Jenkins log/output for this job + def last_build_log(job_name, start = 0) + get( + path: "/job/#{job_name}/#{last_build_id(job_name)}/logText/progressiveText", + params: { start: start } + ).body + end + + # Triggers a build for a given job + # + # @param name [String] the name of the job to trigger a build for + # @param [Hash] params the query parameters as a hash for the build endpoint + def build(name, params: {}) + post(params, path: "/job/#{name}/build") + end + + # Executes a Groovy script against the Jenkins instance + # + # @param script [String] the Groovy script to execute + def execute(script) + post("script=#{script}", path: '/scriptText') + end + + # Sends XML to a given Jenkins endpoint + # This might be useful for filling in gaps in this lib + # + # @param xml [String] the xml to post + # @param params [Hash] the query parameters as a hash + # @param path [String] the path to post to ex: /job/<name>/build + # @return [Typhoeus::Response] + def post_xml(xml, params: {}, path: '') + post(xml, params: params, path: path, headers: { 'Content-Type' => 'text/xml' }) + end + + # Posts data to Jenkins + # This might be useful for filling in gaps in this lib + # + # @param data [String | Hash] the xml to post + # @param params [Hash] the query parameters as a hash + # @param path [String] the path to post to ex: /job/<name>/build + # @param headers [Hash] additional headers to send + # @return [Typhoeus::Response] + def post(data, params: {}, path: '', headers: {}) + get_crumb + RestClient.post( + "#{api_path}#{path}?#{params_to_s(params)}", + data, + headers.merge(full_headers) + ) + end + + # Gets from a Jenkins endpoint + # This might be useful for filling in gaps in this lib + # + # @param path [String] the path to get from ex: /job/<name>/builds/<build_id>/logText/progressiveText + # @param params [Hash] the query parameters as a hash + # @return [Typhoeus::Response] + def get(path: '', params: {}) + get_crumb + RestClient.get( + "#{api_path}#{path}?#{params_to_s(params)}", + full_headers + ) + end + + # configures the Jenkins GitLab plugin + # + # @param url [String] the url for the GitLab instance + # @param access_token [String] an access token for the GitLab instance + # @param secret_id [String] an secret id used for the Jenkins GitLab credentials + # @param hargs [Hash] extra keyword arguments to provide + # @option hargs [String] :connection_name the name to use for the gitlab connection + # @option hargs [Integer] :read_timeout the read timeout for GitLab Jenkins + # @option hargs [Integer] :connection_timeout the connection timeout for GitLab Jenkins + # @option hargs [Boolean] :ignore_ssl_errors whether GitLab Jenkins should ignore SSL errors + # @return [String] the execute response from Jenkins + def configure_gitlab_plugin(url, access_token:, secret_id: SecureRandom.hex(4), **hargs) + configure_secret(access_token, secret_id) + configure_gitlab(url, secret_id, **hargs) + end + + private + + def parse_result(res) + check_network_error(res) + + res.body.scan(/Result: (.*)/)&.dig(0, 0) + end + + def configure_gitlab( + url, + secret_id, + connection_name: 'default', + read_timeout: 10, + connection_timeout: 10, + ignore_ssl_errors: true + ) + res = execute <<~GROOVY + import com.dabsquared.gitlabjenkins.connection.*; + conn = new GitLabConnection( + "#{connection_name}", + "#{url}", + "#{secret_id}", + #{ignore_ssl_errors}, + #{connection_timeout}, + #{read_timeout} + ); + + config = GitLabConnectionConfig.get(); + config.setConnections([conn]); + GROOVY + res.body + end + + def configure_secret(access_token, credential_id) + execute <<~GROOVY + import jenkins.model.Jenkins; + import com.cloudbees.plugins.credentials.domains.Domain; + import org.jenkinsci.plugins.plaincredentials.impl.StringCredentialsImpl; + import com.cloudbees.plugins.credentials.CredentialsScope; + import hudson.util.Secret; + + instance = Jenkins.instance; + domain = Domain.global(); + store = instance.getExtensionList("com.cloudbees.plugins.credentials.SystemCredentialsProvider")[0].getStore(); + + secretText = new StringCredentialsImpl( + CredentialsScope.GLOBAL, + "#{credential_id}", + "GitLab API Token", + Secret.fromString("#{access_token}") + ); + + store.addCredentials(domain, secretText); + GROOVY + end + + def get_crumb + return if @crumb + + response = RestClient.get(crumb_path, auth_headers) + response_body = handle_json_response(response) + @crumb = response_body['crumb'] + end + + def params_to_s(params) + params.each_with_object([]) do |(k, v), memo| + memo << "#{k}=#{v}" + end.join('&') + end + + def full_headers + crumb_headers + .merge(auth_headers) + .merge(cookie_headers) + end + + def crumb_headers + { 'Jenkins-Crumb' => @crumb } + end + + def auth_headers + { 'Authorization' => "Basic #{userpwd}" } + end + + def cookie_headers + { cookies: @cookies } + end + + def userpwd + Base64.encode64("#{@user}:#{@password}") + end + + def api_path + "http://#{@host}:#{port}" + end + + def crumb_path + "#{api_path}/crumbIssuer/api/json" + end + + def port + @port || DEFAULT_SERVER_PORT + end + end + end + end +end diff --git a/qa/qa/vendor/jenkins/helpers.rb b/qa/qa/vendor/jenkins/helpers.rb new file mode 100644 index 00000000000..38175d5687a --- /dev/null +++ b/qa/qa/vendor/jenkins/helpers.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Vendor + module Jenkins + module Helpers + private + + def try_parse(string) + JSON.parse(string) + rescue StandardError => _e + nil + end + + def check_network_error(response) + raise NetworkError, "#{response.code} - #{response.body}" if response.code >= 400 + end + + def handle_json_response(response) + check_network_error(response) + set_cookies(response) + + unless (data = try_parse(response.body)) + raise NotParseableError, "Code: #{response.code}\nBody: #{response.body}" + end + + data + end + + def set_cookies(response) + self.cookies = response.cookies + end + end + end + end +end diff --git a/qa/qa/vendor/jenkins/job.rb b/qa/qa/vendor/jenkins/job.rb new file mode 100644 index 00000000000..46048960fae --- /dev/null +++ b/qa/qa/vendor/jenkins/job.rb @@ -0,0 +1,166 @@ +# frozen_string_literal: true + +module QA + module Vendor + module Jenkins + class Job + include Helpers + + REQUIRED_BUILD_FIELDS = %i[name description shell_command].freeze + + attr_accessor( + :name, + :description, + :keep_deps, + :can_roam, + :disabled, + :repo_url, + :gitlab_connection, + :shell_command + ) + + # Prefer Jenkins::Client#jobs and Jenkins::Client.create_job over this constructor + # + # @param name [String] the name of the job + # @param client [Jenkins::Client] the jenkins client + def initialize(name, client) + @name = name + @client = client + end + + # Saves the Job in Jenkins + def create + validate_required_fields! + + response = @client.post_xml(build, path: '/createItem', params: { name: name }) + + check_network_error(response) + response.body + end + + # Triggers a build for the job + def run + @client.build(@name) + end + + # Returns the jobs last build status + def status + @client.last_build_status(@name) + end + + # Returns the jobs last log + # + # @param start [Integer] the log offset to query + def log(start: 0) + @client.last_build_log(@name, start) + end + + # Returns whether the job is running + # + # @return [Boolean] + def running? + @client.job_running?(@name) + end + + # Returns the count of active builds + # + # @return [Integer] + def active_runs + @client.number_of_jobs_running(@name) + end + + private + + def validate_required_fields! + error = REQUIRED_BUILD_FIELDS.each_with_object("") do |field, memo| + memo << "#{field} is required\n" unless send(field) + end + raise ArgumentError, error unless error.empty? + end + + def build + builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8') do |xml| + xml.project do + xml.actions + xml.description description + xml.keepDependencies false + xml.properties do |props| + build_gitlab_connection(props) + end + xml.canRoam true + xml.disabled false + xml.blockBuildWhenDownstreamBuilding false + xml.blockBuildWhenUpstreamBuilding false + xml.triggers do |triggers| + build_gitlab_triggers(triggers) + end + xml.concurrentBuild false + xml.builders do + xml.send('hudson.tasks.Shell') do + xml.command shell_command + xml.configuredLocalRules + end + end + xml.publishers do |publishers| + build_gitlab_publishers(publishers) + end + xml.buildWrappers + build_scm(xml) + end + end + builder.to_xml + end + + def build_scm(xml) + if repo_url + xml.scm(class: 'hudson.plugins.git.GitSCM') do + xml.userRemoteConfigs do + xml.send('hudson.plugins.git.UserRemoteConfig') do + xml.url repo_url + end + end + xml.branches do + xml.send('hudson.plugins.git.BranchSpec') do + xml.name + end + end + xml.configVersion 2 + xml.doGenerateSubmoduleConfiguration false + xml.gitTool 'Default' + end + end + end + + def build_gitlab_connection(xml) + if gitlab_connection + xml.send('com.dabsquared.gitlabjenkins.connection.GitLabConnectionProperty') do + xml.gitLabConnection gitlab_connection + end + end + end + + def build_gitlab_triggers(xml) + if gitlab_connection + xml.send('com.dabsquared.gitlabjenkins.GitLabPushTrigger') do + xml.spec + xml.triggerOnPush true + xml.triggerOnMergeRequest true + xml.includeBranchesSpec 'main,master' + xml.branchFilterType 'NameBasedFilter' + xml.ciSkip true + end + end + end + + def build_gitlab_publishers(xml) + if gitlab_connection + xml.send('com.dabsquared.gitlabjenkins.publisher.GitLabCommitStatusPublisher') do + xml.name 'jenkins' + xml.markUnstableAsSuccess false + end + end + end + end + end + end +end diff --git a/qa/qa/vendor/jenkins/page/base.rb b/qa/qa/vendor/jenkins/page/base.rb deleted file mode 100644 index 8dfbe7570f8..00000000000 --- a/qa/qa/vendor/jenkins/page/base.rb +++ /dev/null @@ -1,24 +0,0 @@ -# frozen_string_literal: true - -module QA - module Vendor - module Jenkins - module Page - class Base - include Capybara::DSL - include Scenario::Actable - - attr_reader :path - - class << self - attr_accessor :host - end - - def visit! - page.visit URI.join(Base.host, path).to_s - end - end - end - end - end -end diff --git a/qa/qa/vendor/jenkins/page/configure.rb b/qa/qa/vendor/jenkins/page/configure.rb deleted file mode 100644 index da59060152d..00000000000 --- a/qa/qa/vendor/jenkins/page/configure.rb +++ /dev/null @@ -1,48 +0,0 @@ -# frozen_string_literal: true - -require 'capybara/dsl' - -module QA - module Vendor - module Jenkins - module Page - class Configure < Page::Base - def initialize - @path = 'configure' - end - - def visit_and_setup_gitlab_connection(gitlab_host, token_description) - visit! - fill_in '_.name', with: 'GitLab' - find('.setting-name', text: "Gitlab host URL").find(:xpath, "..").find('input').set gitlab_host - - dropdown_element = find('.setting-name', text: "Credentials").find(:xpath, "..").find('select') - - QA::Support::Retrier.retry_until(raise_on_failure: true) do - dropdown_element.select "GitLab API token (#{token_description})" - dropdown_element.value != '' - end - - yield if block_given? - - click_save - end - - def click_test_connection - click_on 'Test Connection' - end - - def has_success? - has_css?('div.ok', text: "Success") - end - - private - - def click_save - click_on 'Save' - end - end - end - end - end -end diff --git a/qa/qa/vendor/jenkins/page/configure_job.rb b/qa/qa/vendor/jenkins/page/configure_job.rb deleted file mode 100644 index 65719795720..00000000000 --- a/qa/qa/vendor/jenkins/page/configure_job.rb +++ /dev/null @@ -1,77 +0,0 @@ -# frozen_string_literal: true - -require 'capybara/dsl' - -module QA - module Vendor - module Jenkins - module Page - class ConfigureJob < Page::Base - attr_accessor :job_name - - def path - "/job/#{@job_name}/configure" - end - - def configure(scm_url:) - set_git_source_code_management_url(scm_url) - set_git_branches_to_build("*/#{Runtime::Env.default_branch}") - click_build_when_change_is_pushed_to_gitlab - set_publish_status_to_gitlab - - Support::Retrier.retry_until(sleep_interval: 0.5) do - click_save - wait_for_configuration_to_save - end - end - - private - - def set_git_source_code_management_url(repository_url) - select_git_source_code_management - set_repository_url(repository_url) - end - - def set_git_branches_to_build(branches) - find('.setting-name', text: "Branch Specifier (blank for 'any')").find(:xpath, "..").find('input').set branches - end - - def click_build_when_change_is_pushed_to_gitlab - find('label', text: 'Build when a change is pushed to GitLab').find(:xpath, "..").find('input').click - end - - def set_publish_status_to_gitlab - click_add_post_build_action - select_publish_build_status_to_gitlab - end - - def click_save - click_on 'Save' - end - - def select_git_source_code_management - find('#radio-block-1').click - end - - def set_repository_url(repository_url) - find('.setting-name', text: "Repository URL").find(:xpath, "..").find('input').set repository_url - end - - def click_add_post_build_action - click_on "Add post-build action" - end - - def select_publish_build_status_to_gitlab - click_link "Publish build status to GitLab" - end - - def wait_for_configuration_to_save - QA::Support::Waiter.wait_until(max_duration: 10, raise_on_failure: false) do - !page.current_url.include?(path) - end - end - end - end - end - end -end diff --git a/qa/qa/vendor/jenkins/page/last_job_console.rb b/qa/qa/vendor/jenkins/page/last_job_console.rb deleted file mode 100644 index 9fcbb8ab956..00000000000 --- a/qa/qa/vendor/jenkins/page/last_job_console.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -require 'capybara/dsl' - -module QA - module Vendor - module Jenkins - module Page - class LastJobConsole < Page::Base - attr_accessor :job_name - - CONSOLE_OUTPUT_SELECTOR = '.console-output' - - def path - "/job/#{@job_name}/lastBuild/console" - end - - def has_successful_build? - # Retry on errors such as: - # Selenium::WebDriver::Error::JavascriptError: - # javascript error: this.each is not a function - Support::Retrier.retry_on_exception(reload_page: page, sleep_interval: 1) do - has_console_output? && console_output.include?('Finished: SUCCESS') - end - end - - def no_failed_status_update? - !console_output.include?('Failed to update Gitlab commit status') - end - - private - - def has_console_output? - page.has_selector?(CONSOLE_OUTPUT_SELECTOR, wait: 1) - end - - def console_output - page.find(CONSOLE_OUTPUT_SELECTOR).text - end - end - end - end - end -end diff --git a/qa/qa/vendor/jenkins/page/login.rb b/qa/qa/vendor/jenkins/page/login.rb deleted file mode 100644 index b18c02b5a44..00000000000 --- a/qa/qa/vendor/jenkins/page/login.rb +++ /dev/null @@ -1,31 +0,0 @@ -# frozen_string_literal: true - -require 'capybara/dsl' - -module QA - module Vendor - module Jenkins - module Page - class Login < Page::Base - def initialize - @path = 'login' - end - - def visit! - super - - QA::Support::Retrier.retry_until(sleep_interval: 3, reload_page: page, max_attempts: 20, raise_on_failure: true) do - page.has_text? 'Welcome to Jenkins!' - end - end - - def login - fill_in 'j_username', with: 'admin' - fill_in 'j_password', with: 'password' - click_on 'Sign in' - end - end - end - end - end -end diff --git a/qa/qa/vendor/jenkins/page/new_credentials.rb b/qa/qa/vendor/jenkins/page/new_credentials.rb deleted file mode 100644 index b0d13973090..00000000000 --- a/qa/qa/vendor/jenkins/page/new_credentials.rb +++ /dev/null @@ -1,50 +0,0 @@ -# frozen_string_literal: true - -require 'capybara/dsl' - -module QA - module Vendor - module Jenkins - module Page - class NewCredentials < Page::Base - def initialize - @path = 'credentials/store/system/domain/_/newCredentials' - end - - def visit_and_set_gitlab_api_token(api_token, description) - visit! - wait_for_page_to_load - select_gitlab_api_token - set_api_token(api_token) - set_description(description) - click_ok - end - - private - - def select_gitlab_api_token - find('.setting-name', text: "Kind").find(:xpath, "..").find('select').select "GitLab API token" - end - - def set_api_token(api_token) - fill_in '_.apiToken', with: api_token - end - - def set_description(description) - fill_in '_.description', with: description - end - - def click_ok - click_on 'OK' - end - - def wait_for_page_to_load - QA::Support::Waiter.wait_until(sleep_interval: 1.0) do - page.has_css?('.setting-name', text: "Description") - end - end - end - end - end - end -end diff --git a/qa/qa/vendor/jenkins/page/new_job.rb b/qa/qa/vendor/jenkins/page/new_job.rb deleted file mode 100644 index 11fa4ca8a53..00000000000 --- a/qa/qa/vendor/jenkins/page/new_job.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -require 'capybara/dsl' - -module QA - module Vendor - module Jenkins - module Page - class NewJob < Page::Base - def initialize - @path = 'newJob' - end - - def visit_and_create_new_job_with_name(new_job_name) - visit! - set_new_job_name(new_job_name) - click_free_style_project - click_ok - end - - private - - def set_new_job_name(new_job_name) - fill_in 'name', with: new_job_name - end - - def click_free_style_project - find('.hudson_model_FreeStyleProject').click - end - - def click_ok - click_on 'OK' - end - end - end - end - end -end diff --git a/qa/spec/scenario/test/sanity/selectors_spec.rb b/qa/spec/scenario/test/sanity/selectors_spec.rb index 2a68dd23219..ecc8e0e0f2c 100644 --- a/qa/spec/scenario/test/sanity/selectors_spec.rb +++ b/qa/spec/scenario/test/sanity/selectors_spec.rb @@ -5,20 +5,26 @@ RSpec.describe QA::Scenario::Test::Sanity::Selectors do before do stub_const('QA::Page::Validator', validator) + + allow(QA::Runtime::Logger).to receive(:warn) + allow(QA::Runtime::Logger).to receive(:info) end context 'when there are errors detected' do + let(:error) { 'some error' } + before do - allow(validator).to receive(:errors).and_return(['some error']) + allow(validator).to receive(:errors).and_return([error]) end - it 'outputs information about errors' do - expect { described_class.perform } - .to output(/some error/).to_stderr + it 'outputs information about errors', :aggregate_failures do + described_class.perform + + expect(QA::Runtime::Logger).to have_received(:warn) + .with(/GitLab QA sanity selectors validation test detected problems/) - expect { described_class.perform } - .to output(/electors validation test detected problems/) - .to_stderr + expect(QA::Runtime::Logger).to have_received(:warn) + .with(/#{error}/) end end diff --git a/qa/spec/service/docker_run/gitlab_runner_spec.rb b/qa/spec/service/docker_run/gitlab_runner_spec.rb index d9f201cf67e..dd4aaf8f0a1 100644 --- a/qa/spec/service/docker_run/gitlab_runner_spec.rb +++ b/qa/spec/service/docker_run/gitlab_runner_spec.rb @@ -33,17 +33,21 @@ module QA end it 'runs non-interactively' do - expect(subject).to have_received(:shell).with(/ --non-interactive /) + expect(subject).to have_received_masked_shell_command(/ --non-interactive /) end it 'sets pertinent information' do - expect(subject).to have_received(:shell).with(/--name #{runner_name} /) - expect(subject).to have_received(:shell).with(/--url #{subject.address} /) - expect(subject).to have_received(:shell).with(/--registration-token #{subject.token} /) + expect(subject).to have_received_masked_shell_command(/--name #{runner_name} /) + expect(subject).to have_received_masked_shell_command(/--url #{subject.address} /) + expect(subject).to have_received_masked_shell_command(/--registration-token \S+/) + end + + it 'masks the registration token' do + expect(subject).to have_received(:shell).with(/#{subject.token}/, mask_secrets: [subject.token]) end it 'runs untagged' do - expect(subject).to have_received(:shell).with(/--run-untagged=true /) + expect(subject).to have_received_masked_shell_command(/--run-untagged=true /) end it 'has no tags' do @@ -51,11 +55,11 @@ module QA end it 'runs daemonized' do - expect(subject).to have_received(:shell).with(/ -d /) + expect(subject).to have_received_masked_shell_command(/ -d /) end it 'cleans itself up' do - expect(subject).to have_received(:shell).with(/ --rm /) + expect(subject).to have_received_masked_shell_command(/ --rm /) end end @@ -65,11 +69,11 @@ module QA end it 'passes --run-untagged=true' do - expect(subject).to have_received(:shell).with(/--run-untagged=true /) + expect(subject).to have_received_masked_shell_command(/--run-untagged=true /) end it 'does not pass tag list' do - expect(subject).not_to have_received(:shell).with(/--tag-list/) + expect(subject).not_to have_received_masked_shell_command(/--tag-list/) end end @@ -82,11 +86,11 @@ module QA end it 'does not pass --run-untagged' do - expect(subject).not_to have_received(:shell).with(/--run-untagged=true/) + expect(subject).not_to have_received_masked_shell_command(/--run-untagged=true/) end it 'passes the tags with comma-separation' do - expect(subject).to have_received(:shell).with(/--tag-list #{tags.join(',')} /) + expect(subject).to have_received_masked_shell_command(/--tag-list #{tags.join(',')} /) end end @@ -116,7 +120,7 @@ module QA it 'defaults to the shell executor' do register - expect(subject).to have_received(:shell).with(/--executor shell /) + expect(subject).to have_received_masked_shell_command(/--executor shell /) end context 'docker' do @@ -127,31 +131,31 @@ module QA end it 'specifies the docker executor' do - expect(subject).to have_received(:shell).with(/--executor docker /) + expect(subject).to have_received_masked_shell_command(/--executor docker /) end it 'mounts the docker socket to the host runner' do - expect(subject).to have_received(:shell).with(%r{-v /var/run/docker.sock:/var/run/docker.sock }) + expect(subject).to have_received_masked_shell_command(%r{-v /var/run/docker.sock:/var/run/docker.sock }) end it 'runs in privileged mode' do - expect(subject).to have_received(:shell).with(/--privileged /) + expect(subject).to have_received_masked_shell_command(/--privileged /) end it 'has a default image' do - expect(subject).to have_received(:shell).with(/--docker-image \b.+\b /) + expect(subject).to have_received_masked_shell_command(/--docker-image \b.+\b /) end it 'does not verify TLS' do - expect(subject).to have_received(:shell).with(/--docker-tlsverify=false /) + expect(subject).to have_received_masked_shell_command(/--docker-tlsverify=false /) end it 'passes privileged mode' do - expect(subject).to have_received(:shell).with(/--docker-privileged=true /) + expect(subject).to have_received_masked_shell_command(/--docker-privileged=true /) end it 'passes the host network' do - expect(subject).to have_received(:shell).with(/--docker-network-mode=#{subject.network}/) + expect(subject).to have_received_masked_shell_command(/--docker-network-mode=#{subject.network}/) end end end @@ -170,5 +174,15 @@ module QA expect(subject.run_untagged).to be(false) end end + + RSpec::Matchers.define "have_received_masked_shell_command" do |cmd| + match do |actual| + expect(actual).to have_received(:shell).with(cmd, mask_secrets: anything) + end + + match_when_negated do |actual| + expect(actual).not_to have_received(:shell).with(cmd, mask_secrets: anything) + end + end end end diff --git a/qa/spec/support/loglinking_spec.rb b/qa/spec/support/loglinking_spec.rb index e02ae45ee93..d1cc76bccc4 100644 --- a/qa/spec/support/loglinking_spec.rb +++ b/qa/spec/support/loglinking_spec.rb @@ -6,6 +6,7 @@ RSpec.describe QA::Support::Loglinking do it 'if correlation_id is empty' do expect(QA::Support::Loglinking.failure_metadata('')).to eq(nil) end + it 'if correlation_id is nil' do expect(QA::Support::Loglinking.failure_metadata(nil)).to eq(nil) end @@ -37,14 +38,14 @@ RSpec.describe QA::Support::Loglinking do describe '.sentry_url' do let(:url_hash) do { - :staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg', - :staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny', - :staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref', - :pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre', - :canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd', - :production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny', - :foo => nil, - nil => nil + :staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg', + :staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny', + :staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref', + :pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre', + :canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd', + :production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny', + :foo => nil, + nil => nil } end @@ -60,14 +61,14 @@ RSpec.describe QA::Support::Loglinking do describe '.kibana_url' do let(:url_hash) do { - :staging => 'https://nonprod-log.gitlab.net/', - :staging_canary => 'https://nonprod-log.gitlab.net/', - :staging_ref => nil, - :pre => nil, - :canary => 'https://log.gprd.gitlab.net/', - :production => 'https://log.gprd.gitlab.net/', - :foo => nil, - nil => nil + :staging => 'https://nonprod-log.gitlab.net/', + :staging_canary => 'https://nonprod-log.gitlab.net/', + :staging_ref => nil, + :pre => nil, + :canary => 'https://log.gprd.gitlab.net/', + :production => 'https://log.gprd.gitlab.net/', + :foo => nil, + nil => nil } end @@ -168,6 +169,7 @@ RSpec.describe QA::Support::Loglinking do expect(QA::Support::Loglinking.canary?).to eq(true) end + it 'and not true returns false' do allow(QA::Support::Loglinking).to receive(:cookies).and_return({ 'gitlab_canary' => { name: 'gitlab_canary', value: 'false' } }) diff --git a/qa/spec/support/page_error_checker_spec.rb b/qa/spec/support/page_error_checker_spec.rb index ab7014f4677..735c0f83ecd 100644 --- a/qa/spec/support/page_error_checker_spec.rb +++ b/qa/spec/support/page_error_checker_spec.rb @@ -113,6 +113,7 @@ RSpec.describe QA::Support::PageErrorChecker do expect(QA::Support::PageErrorChecker.parse_five_c_page_request_id(page).to_str).to eq('req678') end + it 'returns nil if not present' do allow(page).to receive(:html).and_return(error_500_no_code_str) allow(Nokogiri::HTML).to receive(:parse).with(error_500_no_code_str).and_return(NokogiriParse.parse(error_500_no_code_str)) @@ -217,6 +218,7 @@ RSpec.describe QA::Support::PageErrorChecker do expect(QA::Support::PageErrorChecker).to receive(:report!).with(page, 404) QA::Support::PageErrorChecker.check_page_for_error_code(page) end + it 'calls report with 500 if 500 found' do allow(page).to receive(:html).and_return(error_500_str) allow(Nokogiri::HTML).to receive(:parse).with(error_500_str).and_return(NokogiriParse.parse(error_500_str)) @@ -224,6 +226,7 @@ RSpec.describe QA::Support::PageErrorChecker do expect(QA::Support::PageErrorChecker).to receive(:report!).with(page, 500) QA::Support::PageErrorChecker.check_page_for_error_code(page) end + it 'calls report with 500 if GDK backtrace found' do allow(page).to receive(:html).and_return(backtrace_str) allow(Nokogiri::HTML).to receive(:parse).with(backtrace_str).and_return(NokogiriParse.parse(backtrace_str)) @@ -231,6 +234,7 @@ RSpec.describe QA::Support::PageErrorChecker do expect(QA::Support::PageErrorChecker).to receive(:report!).with(page, 500) QA::Support::PageErrorChecker.check_page_for_error_code(page) end + it 'does not call report if 500 found in project name' do allow(page).to receive(:html).and_return(project_name_500_str) allow(Nokogiri::HTML).to receive(:parse).with(project_name_500_str).and_return(NokogiriParse.parse(project_name_500_str)) @@ -238,6 +242,7 @@ RSpec.describe QA::Support::PageErrorChecker do expect(QA::Support::PageErrorChecker).not_to receive(:report!) QA::Support::PageErrorChecker.check_page_for_error_code(page) end + it 'does not call report if no 404, 500 or backtrace found' do allow(page).to receive(:html).and_return(no_error_str) allow(Nokogiri::HTML).to receive(:parse).with(no_error_str).and_return(NokogiriParse.parse(no_error_str)) diff --git a/qa/tasks/knapsack.rake b/qa/tasks/knapsack.rake index fe9a9c4586f..c1225964aef 100644 --- a/qa/tasks/knapsack.rake +++ b/qa/tasks/knapsack.rake @@ -1,6 +1,5 @@ # frozen_string_literal: true -# rubocop:disable Rails/RakeEnvironment namespace :knapsack do desc "Run tests with knapsack runner" task :rspec, [:rspec_args] do |_, args| @@ -16,11 +15,26 @@ namespace :knapsack do exit QA::Specs::KnapsackRunner.run(rspec_args) end - desc "Download latest knapsack report or multiple reports passed via QA_KNAPSACK_REPORTS env variable" - task :download do - next QA::Support::KnapsackReport.download_report unless ENV["QA_KNAPSACK_REPORTS"] + desc "Download latest knapsack reports for parallel jobs" + task :download, [:stage_name] do |_, args| + test_stage_name = args[:stage_name] - ENV["QA_KNAPSACK_REPORTS"].split(",").each do |report_name| + # QA_KNAPSACK_REPORTS remains for changes to be backwards compatible + # TODO: remove and only use automated detection once changes are merged + unless ENV["QA_KNAPSACK_REPORTS"] || test_stage_name + QA::Runtime::Logger.warn("Missing QA_KNAPSACK_REPORTS environment variable or test stage name for autodetection") + next + end + + reports = if test_stage_name + QA::Support::ParallelPipelineJobs + .fetch(stage_name: test_stage_name, access_token: ENV["QA_GITLAB_CI_TOKEN"]) + .map { |job| job.tr(":", "-") } + else + ENV["QA_KNAPSACK_REPORTS"].split(",") + end + + reports.each do |report_name| QA::Support::KnapsackReport.new(report_name).download_report rescue StandardError => e QA::Runtime::Logger.error(e) @@ -37,4 +51,3 @@ namespace :knapsack do QA::Tools::LongRunningSpecReporter.execute end end -# rubocop:enable Rails/RakeEnvironment diff --git a/qa/tasks/reliable_report.rake b/qa/tasks/reliable_report.rake index b4dcc2ebc01..1045fd823ab 100644 --- a/qa/tasks/reliable_report.rake +++ b/qa/tasks/reliable_report.rake @@ -1,8 +1,6 @@ # frozen_string_literal: true -# rubocop:disable Rails/RakeEnvironment desc "Fetch reliable and unreliable spec data and create report" task :reliable_spec_report, [:range, :report_in_issue_and_slack] do |_task, args| QA::Tools::ReliableReport.run(**args) end -# rubocop:enable Rails/RakeEnvironment diff --git a/qa/tasks/vulnerabilities.rake b/qa/tasks/vulnerabilities.rake index 79d6b8683e0..cab2f8e5ca6 100644 --- a/qa/tasks/vulnerabilities.rake +++ b/qa/tasks/vulnerabilities.rake @@ -1,5 +1,4 @@ # frozen_string_literal: true -# rubocop:disable Rails/RakeEnvironment # How to run this rake task? # GITLAB_QA_ACCESS_TOKEN=<access_token> GITLAB_URL="<Gitlab address>" bundle exec rake @@ -26,4 +25,3 @@ namespace :vulnerabilities do vuln.create_vuln_report(args[:project_id], args[:vulnerability_count].to_i) end end -# rubocop:enable Rails/RakeEnvironment |