diff options
Diffstat (limited to 'qa')
114 files changed, 1939 insertions, 506 deletions
diff --git a/qa/Gemfile b/qa/Gemfile index e2951db534a..8b9592a027b 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -4,7 +4,7 @@ gem 'gitlab-qa' gem 'activesupport', '~> 6.0.3.1' # This should stay in sync with the root's Gemfile gem 'capybara', '~> 3.29.0' gem 'capybara-screenshot', '~> 1.0.23' -gem 'rake', '~> 12.3.0' +gem 'rake', '~> 12.3.3' gem 'rspec', '~> 3.7' gem 'selenium-webdriver', '~> 3.12' gem 'airborne', '~> 0.3.4' @@ -16,10 +16,11 @@ gem 'faker', '~> 1.6', '>= 1.6.6' gem 'knapsack', '~> 1.17' gem 'parallel_tests', '~> 2.29' gem 'rotp', '~> 3.1.0' +gem 'timecop', '~> 0.9.1' +gem "parallel", "~> 1.19" -group :test do +group :development do gem 'pry-byebug', '~> 3.5.1', platform: :mri gem "ruby-debug-ide", "~> 0.7.0" gem "debase", "~> 0.2.4.1" - gem 'timecop', '~> 0.9.1' end diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index c2b876e3b04..b88cc47ad94 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -126,9 +126,10 @@ DEPENDENCIES gitlab-qa knapsack (~> 1.17) nokogiri (~> 1.10.9) + parallel (~> 1.19) parallel_tests (~> 2.29) pry-byebug (~> 3.5.1) - rake (~> 12.3.0) + rake (~> 12.3.3) rest-client (~> 2.1.0) rotp (~> 3.1.0) rspec (~> 3.7) diff --git a/qa/README.md b/qa/README.md index 8f41327fb15..7ed4d63a589 100644 --- a/qa/README.md +++ b/qa/README.md @@ -178,11 +178,13 @@ another test has `:ldap` and `:quarantine` metadata. If the tests are run with `--tag smoke --tag quarantine`, only the first test will run. The test with `:ldap` will not run even though it also has `:quarantine`. -### Running tests with a feature flag enabled +### Running tests with a feature flag enabled or disabled -Tests can be run with with a feature flag enabled by using the command-line -option `--enable-feature FEATURE_FLAG`. For example, to enable the feature flag -that enforces Gitaly request limits, you would use the command: +Tests can be run with with a feature flag enabled or disabled by using the command-line +option `--enable-feature FEATURE_FLAG` or `--disable-feature FEATURE_FLAG`. + +For example, to enable the feature flag that enforces Gitaly request limits, +you would use the command: ``` bundle exec bin/qa Test::Instance::All http://localhost:3000 --enable-feature gitaly_enforce_requests_limits @@ -193,9 +195,20 @@ feature flag ([via the API](https://docs.gitlab.com/ee/api/features.html)), run all the tests in the `Test::Instance::All` scenario, and then disable the feature flag again. +Similarly, to disable the feature flag that enforces Gitaly request limits, +you would use the command: + +``` +bundle exec bin/qa Test::Instance::All http://localhost:3000 --disable-feature gitaly_enforce_requests_limits +``` +This will instruct the QA framework to disable the `gitaly_enforce_requests_limits` +feature flag ([via the API](https://docs.gitlab.com/ee/api/features.html)) if not already disabled, +run all the tests in the `Test::Instance::All` scenario, and then enable the +feature flag again if it was enabled earlier. + Note: the QA framework doesn't currently allow you to easily toggle a feature flag during a single test, [as you can in unit tests](https://docs.gitlab.com/ee/development/feature_flags.html#specs), but [that capability is planned](https://gitlab.com/gitlab-org/quality/team-tasks/issues/77). -Note also that the `--` separator isn't used because `--enable-feature` is a QA -framework option, not an `rspec` option. +Note also that the `--` separator isn't used because `--enable-feature` and `--disable-feature` +are QA framework options, not `rspec` options. diff --git a/qa/Rakefile b/qa/Rakefile index 9f547b92bb8..844d8ff898d 100644 --- a/qa/Rakefile +++ b/qa/Rakefile @@ -42,12 +42,6 @@ desc "Generate data and run load tests" task generate_data_and_run_load_test: [:generate_perf_testdata, :run_artillery_load_tests] desc "Deletes test ssh keys a user" -task :delete_test_ssh_keys, [:title_portion, :delete_before] do |t, args| - QA::Tools::DeleteTestSSHKeys.new(args).run -end - -desc "Dry run of deleting test ssh keys for a user. Lists keys to be deleted" -task :delete_test_ssh_keys_dry_run, [:title_portion, :delete_before] do |t, args| - args.with_defaults(dry_run: true) +task :delete_test_ssh_keys, [:title_portion, :delete_before, :dry_run] do |t, args| QA::Tools::DeleteTestSSHKeys.new(args).run end @@ -151,7 +151,6 @@ module QA autoload :Mattermost, 'qa/scenario/test/integration/mattermost' autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage' autoload :SMTP, 'qa/scenario/test/integration/smtp' - autoload :GitalyHA, 'qa/scenario/test/integration/gitaly_ha' end module Sanity @@ -208,6 +207,7 @@ module QA autoload :New, 'qa/page/group/new' autoload :Show, 'qa/page/group/show' autoload :Menu, 'qa/page/group/menu' + autoload :Members, 'qa/page/group/members' module Milestone autoload :Index, 'qa/page/group/milestone/index' @@ -216,7 +216,6 @@ module QA module SubMenus autoload :Common, 'qa/page/group/sub_menus/common' - autoload :Members, 'qa/page/group/sub_menus/members' end module Settings @@ -277,6 +276,11 @@ module QA autoload :Show, 'qa/page/project/job/show' end + module Packages + autoload :Index, 'qa/page/project/packages/index' + autoload :Show, 'qa/page/project/packages/show' + end + module Settings autoload :Advanced, 'qa/page/project/settings/advanced' autoload :Main, 'qa/page/project/settings/main' @@ -315,6 +319,7 @@ module QA autoload :Repository, 'qa/page/project/sub_menus/repository' autoload :Settings, 'qa/page/project/sub_menus/settings' autoload :Project, 'qa/page/project/sub_menus/project' + autoload :Packages, 'qa/page/project/sub_menus/packages' end module Issue @@ -349,6 +354,10 @@ module QA module Metrics autoload :Show, 'qa/page/project/operations/metrics/show' end + + module Incidents + autoload :Index, 'qa/page/project/operations/incidents/index' + end end module Wiki @@ -376,6 +385,10 @@ module QA autoload :Emails, 'qa/page/profile/emails' autoload :Password, 'qa/page/profile/password' autoload :TwoFactorAuth, 'qa/page/profile/two_factor_auth' + + module Accounts + autoload :Show, 'qa/page/profile/accounts/show' + end end module Issuable diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock index d44ccbb5e69..9c7c93fb553 100644 --- a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock +++ b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: rack (2.0.6) - rake (12.3.0) + rake (12.3.3) PLATFORMS ruby @@ -12,4 +12,4 @@ DEPENDENCIES rake BUNDLED WITH - 1.17.1 + 1.17.3 diff --git a/qa/qa/flow/saml.rb b/qa/qa/flow/saml.rb index 676be2beb01..e8007978071 100644 --- a/qa/qa/flow/saml.rb +++ b/qa/qa/flow/saml.rb @@ -18,7 +18,7 @@ module QA end end - def enable_saml_sso(group, saml_idp_service) + def enable_saml_sso(group, saml_idp_service, default_membership_role = 'Guest') page.visit Runtime::Scenario.gitlab_address Page::Main::Login.perform(&:sign_in_using_credentials) unless Page::Main::Menu.perform(&:signed_in?) @@ -29,6 +29,7 @@ module QA EE::Page::Group::Settings::SamlSSO.perform do |saml_sso| saml_sso.set_id_provider_sso_url(saml_idp_service.idp_sso_url) saml_sso.set_cert_fingerprint(saml_idp_service.idp_certificate_fingerprint) + saml_sso.set_default_membership_role(default_membership_role) saml_sso.click_save_changes saml_sso.user_login_url_link_text diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 2c8e362edd6..e0fcb9b0599 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -102,6 +102,10 @@ module QA git_lfs_track_result.to_s + git_add_result.to_s end + def add_tag(tag_name) + run("git tag #{tag_name}").to_s + end + def delete_tag(tag_name) run(%Q{git push origin --delete #{tag_name}}, max_attempts: 3).to_s end @@ -122,6 +126,10 @@ module QA run("git push --all").to_s end + def push_tags_and_branches(branches) + run("git push --tags origin #{branches.join(' ')}").to_s + end + def merge(branch) run("git merge #{branch}") end @@ -204,7 +212,7 @@ module QA alias_method :to_s, :response def success? - exitstatus.zero? + exitstatus == 0 end end diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb index da716ca8e27..37275465221 100644 --- a/qa/qa/page/admin/menu.rb +++ b/qa/qa/page/admin/menu.rb @@ -6,7 +6,7 @@ module QA class Menu < Page::Base view 'app/views/layouts/nav/sidebar/_admin.html.haml' do element :admin_sidebar - element :admin_sidebar_settings_submenu + element :admin_sidebar_settings_submenu_content element :admin_settings_item element :admin_settings_repository_item element :admin_settings_general_item @@ -22,7 +22,7 @@ module QA def go_to_preferences_settings hover_element(:admin_settings_item) do - within_submenu(:admin_sidebar_settings_submenu) do + within_submenu(:admin_sidebar_settings_submenu_content) do click_element :admin_settings_preferences_link end end @@ -30,7 +30,7 @@ module QA def go_to_repository_settings hover_element(:admin_settings_item) do - within_submenu(:admin_sidebar_settings_submenu) do + within_submenu(:admin_sidebar_settings_submenu_content) do click_element :admin_settings_repository_item end end @@ -38,7 +38,7 @@ module QA def go_to_integration_settings hover_element(:admin_settings_item) do - within_submenu(:admin_sidebar_settings_submenu) do + within_submenu(:admin_sidebar_settings_submenu_content) do click_element :integration_settings_link end end @@ -46,7 +46,7 @@ module QA def go_to_general_settings hover_element(:admin_settings_item) do - within_submenu(:admin_sidebar_settings_submenu) do + within_submenu(:admin_sidebar_settings_submenu_content) do click_element :admin_settings_general_item end end @@ -54,7 +54,7 @@ module QA def go_to_metrics_and_profiling_settings hover_element(:admin_settings_item) do - within_submenu(:admin_sidebar_settings_submenu) do + within_submenu(:admin_sidebar_settings_submenu_content) do click_element :admin_settings_metrics_and_profiling_item end end @@ -62,7 +62,7 @@ module QA def go_to_network_settings hover_element(:admin_settings_item) do - within_submenu(:admin_sidebar_settings_submenu) do + within_submenu(:admin_sidebar_settings_submenu_content) do click_element :admin_settings_network_item end end diff --git a/qa/qa/page/admin/settings/general.rb b/qa/qa/page/admin/settings/general.rb index 150775f57d2..7e35902a778 100644 --- a/qa/qa/page/admin/settings/general.rb +++ b/qa/qa/page/admin/settings/general.rb @@ -8,11 +8,11 @@ module QA include QA::Page::Settings::Common view 'app/views/admin/application_settings/general.html.haml' do - element :account_and_limit_settings + element :account_and_limit_settings_content end def expand_account_and_limit(&block) - expand_section(:account_and_limit_settings) do + expand_content(:account_and_limit_settings_content) do Component::AccountAndLimit.perform(&block) end end diff --git a/qa/qa/page/admin/settings/metrics_and_profiling.rb b/qa/qa/page/admin/settings/metrics_and_profiling.rb index e10a92d7a54..41fad942fc4 100644 --- a/qa/qa/page/admin/settings/metrics_and_profiling.rb +++ b/qa/qa/page/admin/settings/metrics_and_profiling.rb @@ -8,11 +8,11 @@ module QA include QA::Page::Settings::Common view 'app/views/admin/application_settings/metrics_and_profiling.html.haml' do - element :performance_bar_settings + element :performance_bar_settings_content end def expand_performance_bar(&block) - expand_section(:performance_bar_settings) do + expand_content(:performance_bar_settings_content) do Component::PerformanceBar.perform(&block) end end diff --git a/qa/qa/page/admin/settings/network.rb b/qa/qa/page/admin/settings/network.rb index 83566d3d1ca..253904788e3 100644 --- a/qa/qa/page/admin/settings/network.rb +++ b/qa/qa/page/admin/settings/network.rb @@ -8,18 +8,18 @@ module QA include QA::Page::Settings::Common view 'app/views/admin/application_settings/network.html.haml' do - element :ip_limits_section - element :outbound_requests_section + element :ip_limits_content + element :outbound_requests_content end def expand_ip_limits(&block) - expand_section(:ip_limits_section) do + expand_content(:ip_limits_content) do Component::IpLimits.perform(&block) end end def expand_outbound_requests(&block) - expand_section(:outbound_requests_section) do + expand_content(:outbound_requests_content) do Component::OutboundRequests.perform(&block) end end diff --git a/qa/qa/page/admin/settings/repository.rb b/qa/qa/page/admin/settings/repository.rb index b7f1deb21bd..82321765e63 100644 --- a/qa/qa/page/admin/settings/repository.rb +++ b/qa/qa/page/admin/settings/repository.rb @@ -8,11 +8,11 @@ module QA include QA::Page::Settings::Common view 'app/views/admin/application_settings/repository.html.haml' do - element :repository_storage_settings + element :repository_storage_settings_content end def expand_repository_storage(&block) - expand_section(:repository_storage_settings) do + expand_content(:repository_storage_settings_content) do Component::RepositoryStorage.perform(&block) end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index f0d4ae45ef8..abd9332ced0 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -141,6 +141,15 @@ module QA end end + # Use this to simulate moving the pointer to an element's coordinate + # and sending a click event. + # This is a helpful workaround when there is a transparent element overlapping + # the target element and so, normal `click_element` on target would raise + # Selenium::WebDriver::Error::ElementClickInterceptedError + def click_element_coordinates(name) + page.driver.browser.action.move_to(find_element(name).native).click.perform + end + # replace with (..., page = self.class) def click_element(name, page = nil, **kwargs) wait_for_requests @@ -169,7 +178,7 @@ module QA end def has_element?(name, **kwargs) - wait_for_requests + wait_for_requests(skip_finished_loading_check: !!kwargs.delete(:skip_finished_loading_check)) disabled = kwargs.delete(:disabled) @@ -209,15 +218,6 @@ module QA has_text?(text.gsub(/\s+/, " "), wait: wait) end - def finished_loading? - wait_for_requests - - # The number of selectors should be able to be reduced after - # migration to the new spinner is complete. - # https://gitlab.com/groups/gitlab-org/-/epics/956 - has_no_css?('.gl-spinner, .fa-spinner, .spinner', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) - end - def finished_loading_block? wait_for_requests diff --git a/qa/qa/page/component/confirm_modal.rb b/qa/qa/page/component/confirm_modal.rb index 039640d207a..25eea8e0d93 100644 --- a/qa/qa/page/component/confirm_modal.rb +++ b/qa/qa/page/component/confirm_modal.rb @@ -20,7 +20,14 @@ module QA fill_element :confirm_input, text end - def click_confirm_button + def wait_for_confirm_button_enabled + wait_until(reload: false) do + !find_element(:confirm_button).disabled? + end + end + + def confirm_transfer + wait_for_confirm_button_enabled click_element :confirm_button end end diff --git a/qa/qa/page/component/new_snippet.rb b/qa/qa/page/component/new_snippet.rb index 18f2e237097..3e5ae29177a 100644 --- a/qa/qa/page/component/new_snippet.rb +++ b/qa/qa/page/component/new_snippet.rb @@ -55,12 +55,10 @@ module QA end def fill_file_name(name) - finished_loading? fill_element :file_name_field, name end def fill_file_content(content) - finished_loading? text_area.set content end diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb index 4ff19c01f1f..b84166ccefd 100644 --- a/qa/qa/page/component/snippet.rb +++ b/qa/qa/page/component/snippet.rb @@ -38,7 +38,7 @@ module QA element :delete_snippet_button end - base.view 'app/assets/javascripts/snippets/components/snippet_blob_view.vue' do + base.view 'app/assets/javascripts/snippets/components/show.vue' do element :clone_button end @@ -100,19 +100,16 @@ module QA end def has_file_content?(file_content) - finished_loading? within_element(:file_content) do has_text?(file_content) end end def click_edit_button - finished_loading? click_element(:snippet_action_button, action: 'Edit') end def click_delete_button - finished_loading? click_element(:snippet_action_button, action: 'Delete') click_element(:delete_snippet_button) # wait for the page to reload after deletion @@ -123,32 +120,27 @@ module QA end def get_repository_uri_http - finished_loading? click_element(:clone_button) Git::Location.new(find_element(:copy_http_url_button)['data-clipboard-text']).uri.to_s end def get_repository_uri_ssh - finished_loading? click_element(:clone_button) Git::Location.new(find_element(:copy_ssh_url_button)['data-clipboard-text']).uri.to_s end def add_comment(comment) - finished_loading? fill_element(:note_field, comment) click_element(:comment_button) end def has_comment_author?(author_username) - finished_loading? within_element(:note_author_content) do has_text?('@' + author_username) end end def has_comment_content?(comment_content) - finished_loading? within_element(:note_content) do has_text?(comment_content) end @@ -161,14 +153,12 @@ module QA end def edit_comment(comment) - finished_loading? click_element(:edit_comment_button) fill_element(:edit_note_field, comment) click_element(:save_comment_button) end def delete_comment(comment) - finished_loading? click_element(:more_actions_dropdown) accept_alert do click_element(:delete_comment_button) diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index c103bc26a36..b9e2383a3eb 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -8,6 +8,17 @@ module QA element :project_filter_form, required: true end + view 'app/views/shared/projects/_project.html.haml' do + element :project_content + element :user_role_content + end + + def has_project_with_access_role?(project_name, access_role) + within_element(:project_content, text: project_name) do + has_element?(:user_role_content, text: access_role) + end + end + def go_to_project(name) filter_by_name(name) diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb index d28b8178c99..c650b8e4f90 100644 --- a/qa/qa/page/dashboard/snippet/edit.rb +++ b/qa/qa/page/dashboard/snippet/edit.rb @@ -20,8 +20,7 @@ module QA end def save_changes - click_element(:submit_button) - wait_until { assert_no_element(:submit_button) } + click_element(:submit_button, Page::Dashboard::Snippet::Show) end private diff --git a/qa/qa/page/dashboard/snippet/show.rb b/qa/qa/page/dashboard/snippet/show.rb index 73e6abe174f..576e287d40d 100644 --- a/qa/qa/page/dashboard/snippet/show.rb +++ b/qa/qa/page/dashboard/snippet/show.rb @@ -6,6 +6,10 @@ module QA module Snippet class Show < Page::Base include Page::Component::Snippet + + view 'app/assets/javascripts/snippets/components/snippet_title.vue' do + element :snippet_title_content, required: true + end end end end diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb new file mode 100644 index 00000000000..dce18ee5c55 --- /dev/null +++ b/qa/qa/page/group/members.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module QA + module Page + module Group + class Members < Page::Base + include QA::Page::Component::Select2 + include Page::Component::UsersSelect + + view 'app/assets/javascripts/vue_shared/components/remove_member_modal.vue' do + element :remove_member_modal_content + end + + view 'app/views/shared/members/_invite_member.html.haml' do + element :member_select_field + element :invite_member_button + end + + view 'app/views/shared/members/_member.html.haml' do + element :member_row + element :access_level_dropdown + element :delete_member_button + element :developer_access_level_link, 'qa_selector: "#{role.downcase}_access_level_link"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck + end + + view 'app/views/groups/group_members/index.html.haml' do + element :invite_group_tab + element :groups_list_tab + element :groups_list + end + + view 'app/views/shared/members/_invite_group.html.haml' do + element :group_select_field + element :invite_group_button + end + + view 'app/views/shared/members/_group.html.haml' do + element :group_row + end + + def select_group(group_name) + click_element :group_select_field + search_and_select(group_name) + end + + def invite_group(group_name) + click_element :invite_group_tab + select_group(group_name) + click_element :invite_group_button + end + + def add_member(username) + select_user :member_select_field, username + click_element :invite_member_button + end + + def update_access_level(username, access_level) + within_element(:member_row, text: username) do + click_element :access_level_dropdown + click_element "#{access_level.downcase}_access_level_link" + end + end + + def remove_member(username) + within_element(:member_row, text: username) do + click_element :delete_member_button + end + + within_element(:remove_member_modal_content) do + click_button("Remove member") + end + end + + def has_existing_group_share?(group_name) + click_element :groups_list_tab + + within_element(:groups_list) do + has_element?(:group_row, text: group_name) + end + end + end + end + end +end diff --git a/qa/qa/page/group/settings/general.rb b/qa/qa/page/group/settings/general.rb index 4a30403fda8..8f5267c3362 100644 --- a/qa/qa/page/group/settings/general.rb +++ b/qa/qa/page/group/settings/general.rb @@ -8,7 +8,7 @@ module QA include ::QA::Page::Settings::Common view 'app/views/groups/edit.html.haml' do - element :permission_lfs_2fa_section + element :permission_lfs_2fa_content end view 'app/views/groups/settings/_permissions.html.haml' do @@ -54,49 +54,49 @@ module QA end def set_lfs_enabled - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content check_element :lfs_checkbox click_element :save_permissions_changes_button end def set_lfs_disabled - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content uncheck_element :lfs_checkbox click_element :save_permissions_changes_button end def set_request_access_enabled - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content check_element :request_access_checkbox click_element :save_permissions_changes_button end def set_request_access_disabled - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content uncheck_element :request_access_checkbox click_element :save_permissions_changes_button end def set_require_2fa_enabled - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content check_element :require_2fa_checkbox click_element :save_permissions_changes_button end def set_require_2fa_disabled - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content uncheck_element :require_2fa_checkbox click_element :save_permissions_changes_button end def set_project_creation_level(value) - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content select_element(:project_creation_level_dropdown, value) click_element :save_permissions_changes_button end def toggle_request_access - expand_section :permission_lfs_2fa_section + expand_content :permission_lfs_2fa_content if find_element(:request_access_checkbox).checked? uncheck_element :request_access_checkbox diff --git a/qa/qa/page/group/sub_menus/members.rb b/qa/qa/page/group/sub_menus/members.rb deleted file mode 100644 index 895da639c02..00000000000 --- a/qa/qa/page/group/sub_menus/members.rb +++ /dev/null @@ -1,51 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Group - module SubMenus - class Members < Page::Base - include Page::Component::UsersSelect - - view 'app/assets/javascripts/vue_shared/components/remove_member_modal.vue' do - element :remove_member_modal_content - end - - view 'app/views/shared/members/_invite_member.html.haml' do - element :member_select_field - element :invite_member_button - end - - view 'app/views/shared/members/_member.html.haml' do - element :member_row - element :access_level_dropdown - element :delete_member_button - element :developer_access_level_link, 'qa_selector: "#{role.downcase}_access_level_link"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck - end - - def add_member(username) - select_user :member_select_field, username - click_element :invite_member_button - end - - def update_access_level(username, access_level) - within_element(:member_row, text: username) do - click_element :access_level_dropdown - click_element "#{access_level.downcase}_access_level_link" - end - end - - def remove_member(username) - within_element(:member_row, text: username) do - click_element :delete_member_button - end - - within_element(:remove_member_modal_content) do - click_button("Remove member") - end - end - end - end - end - end -end diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 416946f44f0..9c63ddee890 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -14,6 +14,9 @@ module QA element :user_avatar, required: true element :user_menu, required: true element :stop_impersonation_link + element :issues_shortcut_button, required: true + element :merge_requests_shortcut_button, required: true + element :todos_shortcut_button, required: true end view 'app/views/layouts/nav/_dashboard.html.haml' do @@ -63,6 +66,18 @@ module QA end end + # To go to one of the popular pages using the provided shortcut buttons within top menu + # @param [Symbol] the name of the element (e.g: `:issues_shortcut button`) + # @example: + # Menu.perform do |menu| + # menu.go_to_page_by_shortcut(:issues_shortcut_button) #=> Go to Issues page using shortcut button + # end + def go_to_page_by_shortcut(button) + within_top_menu do + click_element(button) + end + end + def go_to_admin_area click_admin_area diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb index eda7da89a35..8d0914bac4c 100644 --- a/qa/qa/page/merge_request/new.rb +++ b/qa/qa/page/merge_request/new.rb @@ -5,7 +5,7 @@ module QA module MergeRequest class New < Page::Issuable::New view 'app/views/shared/issuable/_form.html.haml' do - element :issuable_create_button + element :issuable_create_button, required: true end def create_merge_request diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index b9a2bf4ee69..906ad490bb1 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -7,10 +7,6 @@ module QA include Page::Component::Note include Page::Component::Issuable::Sidebar - view 'app/assets/javascripts/mr_tabs_popover/components/popover.vue' do - element :dismiss_popover_button - end - view 'app/assets/javascripts/vue_merge_request_widget/components/mr_widget_header.vue' do element :dropdown_toggle element :download_email_patches @@ -275,7 +271,7 @@ module QA end def wait_for_loading - finished_loading? && has_no_element?(:skeleton_note) + has_no_element?(:skeleton_note) end def click_open_in_web_ide diff --git a/qa/qa/page/profile/accounts/show.rb b/qa/qa/page/profile/accounts/show.rb new file mode 100644 index 00000000000..eec4efe1734 --- /dev/null +++ b/qa/qa/page/profile/accounts/show.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Page + module Profile + module Accounts + class Show < Page::Base + view 'app/views/profiles/accounts/show.html.haml' do + element :delete_account_button, required: true + end + + view 'app/assets/javascripts/profile/account/components/delete_account_modal.vue' do + element :password_confirmation_field + end + + view 'app/assets/javascripts/vue_shared/components/deprecated_modal.vue' do + element :save_changes_button + end + + def delete_account(password) + click_element(:delete_account_button) + + find_element(:password_confirmation_field).set password + click_element(:save_changes_button) + end + end + end + end + end +end diff --git a/qa/qa/page/profile/menu.rb b/qa/qa/page/profile/menu.rb index e7baaf3d40a..41c350f94ef 100644 --- a/qa/qa/page/profile/menu.rb +++ b/qa/qa/page/profile/menu.rb @@ -11,6 +11,7 @@ module QA element :ssh_keys, 'SSH Keys' # rubocop:disable QA/ElementWithPattern element :profile_emails_link element :profile_password_link + element :profile_account_link end def click_access_tokens @@ -25,6 +26,12 @@ module QA end end + def click_account + within_sidebar do + click_element(:profile_account_link) + end + end + def click_emails within_sidebar do click_element(:profile_emails_link) diff --git a/qa/qa/page/profile/two_factor_auth.rb b/qa/qa/page/profile/two_factor_auth.rb index b5a4d04b377..6794825769a 100644 --- a/qa/qa/page/profile/two_factor_auth.rb +++ b/qa/qa/page/profile/two_factor_auth.rb @@ -16,6 +16,8 @@ module QA view 'app/views/profiles/two_factor_auths/_codes.html.haml' do element :proceed_button + element :codes_content + element :code_content end def click_configure_it_later_button @@ -34,6 +36,13 @@ module QA click_element :register_2fa_app_button end + def recovery_codes + code_elements = within_element(:codes_content) do + all_elements(:code_content, minimum: 1) + end + code_elements.map { |code_content| code_content.text } + end + def click_proceed_button click_element :proceed_button end diff --git a/qa/qa/page/project/fork/new.rb b/qa/qa/page/project/fork/new.rb index 49c2205fd08..e2f2e9330dd 100644 --- a/qa/qa/page/project/fork/new.rb +++ b/qa/qa/page/project/fork/new.rb @@ -6,11 +6,11 @@ module QA module Fork class New < Page::Base view 'app/views/projects/forks/_fork_button.html.haml' do - element :fork_namespace_content + element :fork_namespace_button end def choose_namespace(namespace = Runtime::Namespace.path) - click_element(:fork_namespace_content, name: namespace) + click_element(:fork_namespace_button, name: namespace) end end end diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index e0c10220fbc..0a64f01fe98 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -5,8 +5,14 @@ module QA module Project module Issue class Index < Page::Base - view 'app/helpers/projects_helper.rb' do + view 'app/assets/javascripts/issuables_list/components/issuable.vue' do + element :issue_container + element :issue_link + end + + view 'app/assets/javascripts/vue_shared/components/issue/issue_assignees.vue' do element :assignee_link + element :avatar_counter_content end view 'app/views/projects/issues/export_csv/_button.html.haml' do @@ -23,21 +29,12 @@ module QA element :import_from_jira_link end - view 'app/views/projects/issues/_issue.html.haml' do - element :issue - element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern - end - - view 'app/views/shared/issuable/_assignees.html.haml' do - element :avatar_counter - end - view 'app/views/shared/issuable/_nav.html.haml' do element :closed_issues_link end def avatar_counter - find_element(:avatar_counter) + find_element(:avatar_counter_content) end def click_issue_link(title) @@ -80,7 +77,7 @@ module QA end def has_issue?(issue) - has_element? :issue, issue_title: issue.title + has_element? :issue_container, issue_title: issue.title end end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 04f0f34cbbb..5778d0218a7 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -38,23 +38,6 @@ module QA element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern end - view 'app/views/projects/issues/_tabs.html.haml' do - element :designs_tab_content - element :designs_tab_link - element :discussion_tab_content - element :discussion_tab_link - end - - def click_discussion_tab - click_element(:discussion_tab_link) - active_element?(:discussion_tab_content) - end - - def click_designs_tab - click_element(:designs_tab_link) - active_element?(:designs_tab_content) - end - def click_remove_related_issue_button click_element(:remove_related_issue_button) end @@ -97,6 +80,10 @@ module QA select_filter_with_text('Show history only') end + def has_metrics_unfurled? + has_element?(:prometheus_graph_widgets, wait: 30) + end + private def select_filter_with_text(text) diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index 9faf1bd5f8f..16c66ea5761 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -11,6 +11,7 @@ module QA include SubMenus::Operations include SubMenus::Repository include SubMenus::Settings + include SubMenus::Packages view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :activity_link diff --git a/qa/qa/page/project/operations/incidents/index.rb b/qa/qa/page/project/operations/incidents/index.rb new file mode 100644 index 00000000000..fd0c5253a7f --- /dev/null +++ b/qa/qa/page/project/operations/incidents/index.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Operations + module Incidents + class Index < Page::Base + view 'app/assets/javascripts/incidents/components/incidents_list.vue' do + element :create_incident_button + end + + def create_incident + click_element :create_incident_button + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb index c143b55d057..1b9a451c47d 100644 --- a/qa/qa/page/project/operations/kubernetes/add_existing.rb +++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb @@ -20,7 +20,7 @@ module QA end def set_api_url(api_url) - fill_in 'cluster_platform_kubernetes_attributes_api_url', with: api_url + fill_in 'cluster_platform_kubernetes_attributes_api_url', with: QA::Runtime::Env.cluster_api_url || api_url end def set_ca_certificate(ca_certificate) diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb index e1612718883..3bb51d2d579 100644 --- a/qa/qa/page/project/operations/kubernetes/show.rb +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -10,7 +10,7 @@ module QA element :ingress_ip_address, 'id="ingress-endpoint"' # rubocop:disable QA/ElementWithPattern end - view 'app/views/clusters/clusters/_gitlab_integration_form.html.haml' do + view 'app/assets/javascripts/clusters/forms/components/integration_form.vue' do element :integration_status_toggle, required: true element :base_domain_field, required: true element :save_changes_button, required: true @@ -56,7 +56,7 @@ module QA def await_installed(application_name) within_element(application_name) do - has_element?(:uninstall_button, application: application_name, wait: 300) + has_element?(:uninstall_button, application: application_name, wait: 300, skip_finished_loading_check: true) end end diff --git a/qa/qa/page/project/operations/metrics/show.rb b/qa/qa/page/project/operations/metrics/show.rb index e9e4923a0e2..22d22af5a9a 100644 --- a/qa/qa/page/project/operations/metrics/show.rb +++ b/qa/qa/page/project/operations/metrics/show.rb @@ -18,10 +18,14 @@ module QA view 'app/assets/javascripts/monitoring/components/dashboard_header.vue' do element :dashboards_filter_dropdown element :environments_dropdown - element :edit_dashboard_button element :range_picker_dropdown end + view 'app/assets/javascripts/monitoring/components/dashboard_actions_menu.vue' do + element :actions_menu_dropdown + element :edit_dashboard_button_enabled + end + view 'app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue' do element :duplicate_dashboard_filename_field end @@ -54,14 +58,16 @@ module QA end def has_edit_dashboard_enabled? - within_element :prometheus_graphs do - has_element? :edit_dashboard_button + click_element :actions_menu_dropdown + + within_element :actions_menu_dropdown do + has_element? :edit_dashboard_button_enabled end end def duplicate_dashboard(save_as = 'test_duplication.yml', commit_option = 'Commit to master branch') - click_element :dashboards_filter_dropdown - click_on 'Duplicate dashboard' + click_element :actions_menu_dropdown + click_on 'Duplicate current dashboard' fill_element :duplicate_dashboard_filename_field, "#{SecureRandom.hex(8)}-#{save_as}" choose commit_option within('.modal-content') { click_button(class: 'btn-success') } diff --git a/qa/qa/page/project/packages/index.rb b/qa/qa/page/project/packages/index.rb new file mode 100644 index 00000000000..3f8cc6035bc --- /dev/null +++ b/qa/qa/page/project/packages/index.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Packages + class Index < QA::Page::Base + view 'app/views/projects/packages/packages/_legacy_package_list.html.haml' do + element :package_row + element :package_link + end + + def click_package(name) + click_element(:package_link, text: name) + end + + def has_package?(name) + has_element?(:package_link, text: name) + end + + def has_no_package?(name) + has_no_element?(:package_link, text: name) + end + end + end + end + end +end diff --git a/qa/qa/page/project/packages/show.rb b/qa/qa/page/project/packages/show.rb new file mode 100644 index 00000000000..59e9a3752c7 --- /dev/null +++ b/qa/qa/page/project/packages/show.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Packages + class Show < QA::Page::Base + view 'app/assets/javascripts/packages/details/components/app.vue' do + element :delete_button + element :delete_modal_button + element :package_information_content + end + + def has_package_info?(name, version) + has_element?(:package_information_content, text: /#{name}.*#{version}/) + end + + def click_delete + click_element(:delete_button) + wait_for_animated_element(:delete_modal_button) + click_element(:delete_modal_button) + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb index d6e004e827e..960d6c221b5 100644 --- a/qa/qa/page/project/settings/advanced.rb +++ b/qa/qa/page/project/settings/advanced.rb @@ -17,6 +17,7 @@ module QA view 'app/views/projects/settings/_archive.html.haml' do element :archive_project_link element :unarchive_project_link + element :archive_project_content end view 'app/views/projects/_export.html.haml' do @@ -42,13 +43,19 @@ module QA end def transfer_project!(project_name, namespace) - expand_select_list - # Workaround for a failure to search when there are no spaces around the / - # https://gitlab.com/gitlab-org/gitlab/-/issues/218965 - select_transfer_option(namespace.gsub(/([^\s])\/([^\s])/, '\1 / \2')) + # Retry added here due to seldom seen inconsistent UI state issue: + # https://gitlab.com/gitlab-org/gitlab/-/issues/231242 + retry_on_exception do + click_element_coordinates(:archive_project_content) + expand_select_list + # Workaround for a failure to search when there are no spaces around the / + # https://gitlab.com/gitlab-org/gitlab/-/issues/218965 + select_transfer_option(namespace.gsub(/([^\s])\/([^\s])/, '\1 / \2')) + end + click_element(:transfer_button) fill_confirmation_text(project_name) - click_confirm_button + confirm_transfer end def click_export_project_link diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb index a7a0f6f57b6..7a910233d12 100644 --- a/qa/qa/page/project/settings/ci_cd.rb +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -15,25 +15,25 @@ module QA end def expand_general_pipelines(&block) - expand_section(:general_pipelines_settings_content) do + expand_content(:general_pipelines_settings_content) do Settings::GeneralPipelines.perform(&block) end end def expand_runners_settings(&block) - expand_section(:runners_settings_content) do + expand_content(:runners_settings_content) do Settings::Runners.perform(&block) end end def expand_ci_variables(&block) - expand_section(:variables_settings_content) do + expand_content(:variables_settings_content) do Settings::CiVariables.perform(&block) end end def expand_auto_devops(&block) - expand_section(:autodevops_settings_content) do + expand_content(:autodevops_settings_content) do Settings::AutoDevops.perform(&block) end end diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index de268b14aa2..aef9800e876 100644 --- a/qa/qa/page/project/settings/ci_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -23,7 +23,7 @@ module QA end def fill_variable(key, value, masked) - fill_element :ci_variable_key_field, key + within_element(:ci_variable_key_field) { find('input').set key } fill_element :ci_variable_value_field, value click_ci_variable_save_button end diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index 880711770c0..3cd558691e1 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -37,19 +37,19 @@ module QA end def expand_advanced_settings(&block) - expand_section(:advanced_settings) do + expand_content(:advanced_settings) do Advanced.perform(&block) end end def expand_merge_requests_settings(&block) - expand_section(:merge_request_settings) do + expand_content(:merge_request_settings) do MergeRequest.perform(&block) end end def expand_visibility_project_features_permissions(&block) - expand_section(:visibility_features_permissions_content) do + expand_content(:visibility_features_permissions_content) do VisibilityFeaturesPermissions.perform(&block) end end diff --git a/qa/qa/page/project/settings/operations.rb b/qa/qa/page/project/settings/operations.rb index b39b8f92cc7..12dcb064807 100644 --- a/qa/qa/page/project/settings/operations.rb +++ b/qa/qa/page/project/settings/operations.rb @@ -12,7 +12,7 @@ module QA end def expand_incidents(&block) - expand_section(:incidents_settings_content) do + expand_content(:incidents_settings_content) do Settings::Incidents.perform(&block) end end diff --git a/qa/qa/page/project/settings/protected_branches.rb b/qa/qa/page/project/settings/protected_branches.rb index 9d302acb058..7315bfb76a5 100644 --- a/qa/qa/page/project/settings/protected_branches.rb +++ b/qa/qa/page/project/settings/protected_branches.rb @@ -17,7 +17,7 @@ module QA element :allowed_to_merge_dropdown end - view 'app/views/projects/protected_branches/_update_protected_branch.html.haml' do + view 'app/views/shared/projects/protected_branches/_update_protected_branch.html.haml' do element :allowed_to_merge end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index fd3a590c2c1..407c131fa73 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -8,19 +8,19 @@ module QA include QA::Page::Settings::Common view 'app/views/projects/protected_branches/shared/_index.html.haml' do - element :protected_branches_settings + element :protected_branches_settings_content end view 'app/views/projects/mirrors/_mirror_repos.html.haml' do - element :mirroring_repositories_settings_section + element :mirroring_repositories_settings_content end view 'app/views/shared/deploy_tokens/_index.html.haml' do - element :deploy_tokens_settings + element :deploy_tokens_settings_content end view 'app/views/shared/deploy_keys/_index.html.haml' do - element :deploy_keys_settings + element :deploy_keys_settings_content end view 'app/views/projects/protected_tags/shared/_index.html.haml' do @@ -28,31 +28,31 @@ module QA end def expand_deploy_tokens(&block) - expand_section(:deploy_tokens_settings) do + expand_content(:deploy_tokens_settings_content) do Settings::DeployTokens.perform(&block) end end def expand_deploy_keys(&block) - expand_section(:deploy_keys_settings) do + expand_content(:deploy_keys_settings_content) do Settings::DeployKeys.perform(&block) end end def expand_protected_branches(&block) - expand_section(:protected_branches_settings) do + expand_content(:protected_branches_settings_content) do ProtectedBranches.perform(&block) end end def expand_mirroring_repositories(&block) - expand_section(:mirroring_repositories_settings_section) do + expand_content(:mirroring_repositories_settings_content) do MirroringRepositories.perform(&block) end end def expand_protected_tags(&block) - expand_section(:protected_tag_settings_content) do + expand_content(:protected_tag_settings_content) do ProtectedTags.perform(&block) end end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 2354a0d9332..22c2ed2a0c2 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -143,6 +143,10 @@ module QA click_element :web_ide_button end + def has_edit_fork_button? + has_element?(:web_ide_button, text: 'Edit fork in Web IDE') + end + def project_name find_element(:project_name_content).text end diff --git a/qa/qa/page/project/snippet/new.rb b/qa/qa/page/project/snippet/new.rb index 7431d6c1bf8..47200ba5fda 100644 --- a/qa/qa/page/project/snippet/new.rb +++ b/qa/qa/page/project/snippet/new.rb @@ -14,6 +14,7 @@ module QA def click_create_first_snippet finished_loading? + # The svg takes a fraction of a second to load after which the # "New snippet" button shifts up a bit. This can cause # webdriver to miss the hit so we wait for the svg to load before diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb index ff9c8a21174..042994062c7 100644 --- a/qa/qa/page/project/sub_menus/operations.rb +++ b/qa/qa/page/project/sub_menus/operations.rb @@ -17,6 +17,7 @@ module QA element :operations_link element :operations_environments_link element :operations_metrics_link + element :operations_incidents_link end end end @@ -45,6 +46,14 @@ module QA end end + def go_to_operations_incidents + hover_operations do + within_submenu do + click_element(:operations_incidents_link) + end + end + end + private def hover_operations diff --git a/qa/qa/page/project/sub_menus/packages.rb b/qa/qa/page/project/sub_menus/packages.rb new file mode 100644 index 00000000000..9ea045a99f5 --- /dev/null +++ b/qa/qa/page/project/sub_menus/packages.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module SubMenus + module Packages + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + view 'app/views/layouts/nav/sidebar/_project_packages_link.html.haml' do + element :packages_link + end + end + end + + def click_packages_link + within_sidebar do + click_element :packages_link + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index b46d2d32f1f..b962b0c673b 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -59,12 +59,25 @@ module QA element :rename_move_button end + view 'app/views/shared/_confirm_fork_modal.html.haml' do + element :fork_project_button + element :confirm_fork_modal + end + + view 'app/assets/javascripts/ide/components/ide_project_header.vue' do + element :project_path_content + end + def has_file?(file_name) within_element(:file_list) do page.has_content? file_name end end + def has_project_path?(project_path) + has_element?(:project_path_content, project_path: project_path) + end + def create_new_file_from_template(file_name, template) click_element(:new_file, Page::Component::WebIDE::Modal::CreateNewFile) @@ -91,7 +104,7 @@ module QA end end - def commit_changes + def commit_changes(open_merge_request: false) # Clicking :begin_commit_button switches from the # edit to the commit view click_element :begin_commit_button @@ -107,19 +120,23 @@ module QA has_element?(:commit_button) end - # Click :commit_button and keep retrying just in case part of the - # animation is still in process even when the buttons have the - # expected visibility. - commit_success_msg_shown = retry_until(sleep_interval: 5) do - click_element(:commit_to_current_branch_radio) if has_element?(:commit_to_current_branch_radio) - click_element(:commit_button) if has_element?(:commit_button) - - wait_until(reload: false) do - has_text?('Your changes have been committed') + if open_merge_request + click_element(:commit_button, Page::MergeRequest::New) + else + # Click :commit_button and keep retrying just in case part of the + # animation is still in process even when the buttons have the + # expected visibility. + commit_success_msg_shown = retry_until(sleep_interval: 5) do + click_element(:commit_to_current_branch_radio) if has_element?(:commit_to_current_branch_radio) + click_element(:commit_button) if has_element?(:commit_button) + + wait_until(reload: false) do + has_text?('Your changes have been committed') + end end - end - raise "The changes do not appear to have been committed successfully." unless commit_success_msg_shown + raise "The changes do not appear to have been committed successfully." unless commit_success_msg_shown + end end def add_to_modified_content(content) @@ -136,12 +153,21 @@ module QA end def create_first_file(file_name) - finished_loading? click_element(:first_file_button, Page::Component::WebIDE::Modal::CreateNewFile) fill_element(:file_name_field, file_name) click_button('Create file') end + def add_file(file_name, file_text) + click_element(:new_file, Page::Component::WebIDE::Modal::CreateNewFile) + fill_element(:file_name_field, file_name) + click_button('Create file') + wait_until(reload: false) { has_file?(file_name) } + within_element(:editor_container) do + find('textarea.inputarea').click.set(file_text) + end + end + def rename_file(file_name, new_file_name) click_element(:file_name_content, text: file_name) click_element(:dropdown_button) @@ -149,6 +175,17 @@ module QA fill_element(:file_name_field, new_file_name) click_button('Rename file') end + + def fork_project! + wait_until(reload: false) do + has_element?(:confirm_fork_modal) + end + click_element(:fork_project_button) + # wait for the fork to be created + wait_until(reload: true) do + has_element?(:file_list) + end + end end end end diff --git a/qa/qa/page/project/wiki/sidebar.rb b/qa/qa/page/project/wiki/sidebar.rb index dc27c23e4c3..3e1edcbbefb 100644 --- a/qa/qa/page/project/wiki/sidebar.rb +++ b/qa/qa/page/project/wiki/sidebar.rb @@ -18,6 +18,10 @@ module QA base.view 'app/views/shared/wikis/_sidebar_wiki_page.html.haml' do element :wiki_page_link end + + base.view 'app/views/shared/wikis/_wiki_directory.html.haml' do + element :wiki_directory_content + end end def click_clone_repository @@ -35,6 +39,10 @@ module QA def has_page_listed?(page_title) has_element? :wiki_page_link, page_name: page_title end + + def has_directory?(directory) + has_element? :wiki_directory_content, text: directory + end end end end diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb index 55477db8804..3f7aa837d3c 100644 --- a/qa/qa/page/search/results.rb +++ b/qa/qa/page/search/results.rb @@ -16,7 +16,7 @@ module QA end view 'app/views/shared/projects/_project.html.haml' do - element :project + element :project_content end def switch_to_code @@ -40,7 +40,7 @@ module QA end def has_project?(project_name) - has_element?(:project, project_name: project_name) + has_element?(:project_content, project_name: project_name) end private diff --git a/qa/qa/page/settings/common.rb b/qa/qa/page/settings/common.rb index 6989e8125d3..f63c987c3b4 100644 --- a/qa/qa/page/settings/common.rb +++ b/qa/qa/page/settings/common.rb @@ -7,14 +7,13 @@ module QA # Click the Expand button present in the specified section # # @param [Symbol] element_name `element` name defined in a `view` block - def expand_section(element_name) + def expand_content(element_name) within_element(element_name) do # Because it is possible to click the button before the JS toggle code is bound wait_until(reload: false) do click_button 'Expand' unless has_css?('button', text: 'Collapse', wait: 1) has_content?('Collapse') - finished_loading? end yield if block_given? diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb index 75dcb4db55f..ed9acb6edb7 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -18,10 +18,12 @@ module QA attribute :id attribute :name attribute :runners_token + attribute :require_two_factor_authentication def initialize @path = Runtime::Namespace.name @description = "QA test run at #{Runtime::Namespace.time}" + @require_two_factor_authentication = false end def fabricate! @@ -72,7 +74,8 @@ module QA parent_id: sandbox.id, path: path, name: path, - visibility: 'public' + visibility: 'public', + require_two_factor_authentication: @require_two_factor_authentication } end diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb index 5c61cc29236..78a24cdb677 100644 --- a/qa/qa/resource/kubernetes_cluster/project_cluster.rb +++ b/qa/qa/resource/kubernetes_cluster/project_cluster.rb @@ -5,7 +5,7 @@ module QA module KubernetesCluster class ProjectCluster < Base attr_writer :cluster, - :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain + :install_ingress, :install_prometheus, :install_runner, :domain attribute :project do Resource::Project.fabricate! @@ -36,33 +36,27 @@ module QA cluster_page.add_cluster! end - if @install_helm_tiller - Page::Project::Operations::Kubernetes::Show.perform do |show| - # We must wait a few seconds for permissions to be set up correctly for new cluster - sleep 10 + Page::Project::Operations::Kubernetes::Show.perform do |show| + # We must wait a few seconds for permissions to be set up correctly for new cluster + sleep 25 - # Open applications tab - show.open_applications + # Open applications tab + show.open_applications - # Helm must be installed before everything else - show.install!(:helm) - show.await_installed(:helm) + show.install!(:ingress) if @install_ingress + show.install!(:prometheus) if @install_prometheus + show.install!(:runner) if @install_runner - show.install!(:ingress) if @install_ingress - show.install!(:prometheus) if @install_prometheus - show.install!(:runner) if @install_runner + show.await_installed(:ingress) if @install_ingress + show.await_installed(:prometheus) if @install_prometheus + show.await_installed(:runner) if @install_runner - show.await_installed(:ingress) if @install_ingress - show.await_installed(:prometheus) if @install_prometheus - show.await_installed(:runner) if @install_runner + if @install_ingress + populate(:ingress_ip) - if @install_ingress - populate(:ingress_ip) - - show.open_details - show.set_domain("#{ingress_ip}.nip.io") - show.save_domain - end + show.open_details + show.set_domain("#{ingress_ip}.nip.io") + show.save_domain end end end diff --git a/qa/qa/resource/members.rb b/qa/qa/resource/members.rb index 4ebed37ca23..52928afa7db 100644 --- a/qa/qa/resource/members.rb +++ b/qa/qa/resource/members.rb @@ -8,9 +8,12 @@ module QA # module Members def add_member(user, access_level = AccessLevel::DEVELOPER) - QA::Runtime::Logger.debug(%Q[Adding user #{user.username} to #{full_path} #{self.class.name}]) + Support::Retrier.retry_until do + QA::Runtime::Logger.debug(%Q[Adding user #{user.username} to #{full_path} #{self.class.name}]) - post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level } + response = post Runtime::API::Request.new(api_client, api_members_path).url, { user_id: user.id, access_level: access_level } + response.code == QA::Support::Api::HTTP_STATUS_CREATED + end end def remove_member(user) diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 358e87b0eb9..eba8ada50ab 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -103,6 +103,20 @@ module QA response.any? { |file| file[:path] == file_path } end + def has_branches?(branches) + branches.all? do |branch| + response = get(Runtime::API::Request.new(api_client, "#{api_repository_branches_path}/#{branch}").url) + response.code == HTTP_STATUS_OK + end + end + + def has_tags?(tags) + tags.all? do |tag| + response = get(Runtime::API::Request.new(api_client, "#{api_repository_tags_path}/#{tag}").url) + response.code == HTTP_STATUS_OK + end + end + def api_get_path "/projects/#{CGI.escape(path_with_namespace)}" end @@ -123,10 +137,18 @@ module QA "#{api_get_path}/runners" end + def api_commits_path + "#{api_get_path}/repository/commits" + end + def api_repository_branches_path "#{api_get_path}/repository/branches" end + def api_repository_tags_path + "#{api_get_path}/repository/tags" + end + def api_repository_tree_path "#{api_get_path}/repository/tree" end @@ -176,6 +198,10 @@ module QA raise Runtime::API::RepositoryStorageMoves::RepositoryStorageMovesError, 'Timed out while waiting for the repository storage move to finish' end + def commits + parse_body(get(Runtime::API::Request.new(api_client, api_commits_path).url)) + end + def import_status response = get Runtime::API::Request.new(api_client, "/projects/#{id}/import").url @@ -204,6 +230,10 @@ module QA parse_body(get(Runtime::API::Request.new(api_client, api_repository_branches_path).url)) end + def repository_tags + parse_body(get(Runtime::API::Request.new(api_client, api_repository_tags_path).url)) + end + def repository_tree parse_body(get(Runtime::API::Request.new(api_client, api_repository_tree_path).url)) end diff --git a/qa/qa/resource/repository/commit.rb b/qa/qa/resource/repository/commit.rb index 3243eacdb28..d15210aa736 100644 --- a/qa/qa/resource/repository/commit.rb +++ b/qa/qa/resource/repository/commit.rb @@ -59,14 +59,19 @@ module QA @update_files = files end - def resource_web_url(resource) + # If `actions` are specified, it performs the actions to create, + # update, or delete commits. If no actions are specified it + # gets existing commits. + def fabricate_via_api! + return api_get if actions.empty? + + super + rescue ResourceNotFoundError super - rescue ResourceURLMissingError - # this particular resource does not expose a web_url property end def api_get_path - "#{api_post_path}/#{@sha}" + api_post_path end def api_post_path diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index 317d70ef2c3..d4e394954ce 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -14,6 +14,7 @@ module QA def initialize self.title = Time.now.to_f + @expires_at = Date.today + 2 end def key diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index 41908a71cf9..462da743318 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -87,6 +87,8 @@ module QA def api_delete_path "/users/#{id}" + rescue NoValueError + "/users/#{fetch_id(username)}" end def api_get_path diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index d29571df981..e4de033c309 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -6,6 +6,8 @@ module QA class Client attr_reader :address, :user + AuthorizationError = Class.new(RuntimeError) + def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true, user: nil, ip_limits: false) @address = address @personal_access_token = personal_access_token diff --git a/qa/qa/runtime/api/repository_storage_moves.rb b/qa/qa/runtime/api/repository_storage_moves.rb index c94a693289f..d0211d3f66d 100644 --- a/qa/qa/runtime/api/repository_storage_moves.rb +++ b/qa/qa/runtime/api/repository_storage_moves.rb @@ -10,16 +10,24 @@ module QA RepositoryStorageMovesError = Class.new(RuntimeError) def has_status?(project, status, destination_storage = Env.additional_repository_storage) - all.any? do |move| - move[:project][:path_with_namespace] == project.path_with_namespace && + find_any do |move| + next unless move[:project][:path_with_namespace] == project.path_with_namespace + + QA::Runtime::Logger.debug("Move data: #{move}") + move[:state] == status && move[:destination_storage_name] == destination_storage end end - def all + def find_any Logger.debug('Getting repository storage moves') - parse_body(get(Request.new(api_client, '/project_repository_storage_moves').url)) + + Support::Waiter.wait_until do + with_paginated_response_body(Request.new(api_client, '/project_repository_storage_moves', per_page: '100').url) do |page| + break true if page.any? { |item| yield item } + end + end end private diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 804ebf27851..cbfce95d409 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -61,6 +61,10 @@ module QA ENV['QA_ADDITIONAL_REPOSITORY_STORAGE'] end + def non_cluster_repository_storage + ENV['QA_GITALY_NON_CLUSTER_STORAGE'] || 'gitaly' + end + def praefect_repository_storage ENV['QA_PRAEFECT_REPOSITORY_STORAGE'] end @@ -107,6 +111,10 @@ module QA ENV['CI'] || ENV['CI_SERVER'] end + def cluster_api_url + ENV['CLUSTER_API_URL'] + end + def qa_cookies ENV['QA_COOKIES'] && ENV['QA_COOKIES'].split(';') end diff --git a/qa/qa/scenario/shared_attributes.rb b/qa/qa/scenario/shared_attributes.rb index bb45c4ce4cb..e2eaca42277 100644 --- a/qa/qa/scenario/shared_attributes.rb +++ b/qa/qa/scenario/shared_attributes.rb @@ -7,6 +7,7 @@ module QA attribute :gitlab_address, '--address URL', 'Address of the instance to test' attribute :enable_feature, '--enable-feature FEATURE_FLAG', 'Enable a feature before running tests' + attribute :disable_feature, '--disable-feature FEATURE_FLAG', 'Disable a feature before running tests' attribute :parallel, '--parallel', 'Execute tests in parallel' attribute :loop, '--loop', 'Execute test repeatedly' end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index 74d4c8f8757..0d517dffee8 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -30,6 +30,8 @@ module QA Runtime::Feature.enable(options[:enable_feature]) if options.key?(:enable_feature) + Runtime::Feature.disable(options[:disable_feature]) if options.key?(:disable_feature) && (@feature_enabled = Runtime::Feature.enabled?(options[:disable_feature])) + Specs::Runner.perform do |specs| specs.tty = true specs.tags = self.class.focus @@ -37,6 +39,7 @@ module QA end ensure Runtime::Feature.disable(options[:enable_feature]) if options.key?(:enable_feature) + Runtime::Feature.enable(options[:disable_feature]) if options.key?(:disable_feature) && @feature_enabled end def extract_option(name, options, args) diff --git a/qa/qa/scenario/test/integration/gitaly_ha.rb b/qa/qa/scenario/test/integration/gitaly_ha.rb deleted file mode 100644 index dbca1a1dd6d..00000000000 --- a/qa/qa/scenario/test/integration/gitaly_ha.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module QA - module Scenario - module Test - module Integration - class GitalyHA < Test::Instance::All - tags :gitaly_ha - end - end - end - end -end diff --git a/qa/qa/service/cluster_provider/k3s.rb b/qa/qa/service/cluster_provider/k3s.rb index 165de795683..cf916d148da 100644 --- a/qa/qa/service/cluster_provider/k3s.rb +++ b/qa/qa/service/cluster_provider/k3s.rb @@ -10,6 +10,7 @@ module QA def setup @k3s = Service::DockerRun::K3s.new.tap do |k3s| + k3s.remove! k3s.register! shell "kubectl config set-cluster k3s --server https://#{k3s.host_name}:6443 --insecure-skip-tls-verify" diff --git a/qa/qa/service/docker_run/gitlab_runner.rb b/qa/qa/service/docker_run/gitlab_runner.rb index 6022ee4ceab..e15047a0f1d 100644 --- a/qa/qa/service/docker_run/gitlab_runner.rb +++ b/qa/qa/service/docker_run/gitlab_runner.rb @@ -92,7 +92,7 @@ module QA CMD end - # Ping CloudFlare DNS, should fail + # Ping Cloudflare DNS, should fail # Ping Registry, should fail to resolve def prove_airgap gitlab_ip = Resolv.getaddress 'registry.gitlab.com' diff --git a/qa/qa/service/docker_run/k3s.rb b/qa/qa/service/docker_run/k3s.rb index da254497ff0..07211b220f1 100644 --- a/qa/qa/service/docker_run/k3s.rb +++ b/qa/qa/service/docker_run/k3s.rb @@ -33,10 +33,12 @@ module QA --name #{@name} --publish 6443:6443 --privileged - #{@image} server --cluster-secret some-secret + #{@image} server + --cluster-secret some-secret + --no-deploy traefik CMD - command.gsub!("--network #{network} ", '') unless QA::Runtime::Env.running_in_ci? + command.gsub!("--network #{network} --hostname #{host_name}", '') unless QA::Runtime::Env.running_in_ci? shell command end diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index a0433689e99..1f1761100c8 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -5,8 +5,12 @@ module QA class PraefectManager include Service::Shellout + attr_accessor :gitlab + + PrometheusQueryError = Class.new(StandardError) + def initialize - @gitlab = 'gitlab-gitaly-ha' + @gitlab = 'gitlab-gitaly-cluster' @praefect = 'praefect' @postgres = 'postgres' @primary_node = 'gitaly1' @@ -15,23 +19,37 @@ module QA @virtual_storage = 'default' end - def enable_writes - shell "docker exec #{@praefect} bash -c '/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml enable-writes -virtual-storage #{@virtual_storage}'" + # Executes the praefect `dataloss` command. + # + # @return [Boolean] whether dataloss has occurred + def dataloss? + wait_until_shell_command_matches(dataloss_command, /Outdated repositories/) end def replicated?(project_id) - shell %(docker exec gitlab-gitaly-ha bash -c 'gitlab-rake "gitlab:praefect:replicas[#{project_id}]"') do |line| - # The output of the rake task looks something like this: - # - # Project name | gitaly1 (primary) | gitaly2 | gitaly3 - # ---------------------------------------------------------------------------------------------------------------------------------------------------------------- - # gitaly_cluster-3aff1f2bd14e6c98 | 23c4422629234d62b62adacafd0a33a8364e8619 | 23c4422629234d62b62adacafd0a33a8364e8619 | 23c4422629234d62b62adacafd0a33a8364e8619 - # - # We want to confirm that the checksums are identical - break line.split('|').map(&:strip)[1..3].uniq.one? if line.start_with?("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) + # The output of the rake task looks something like this: + # + # Project name | gitaly1 (primary) | gitaly2 | gitaly3 + # ---------------------------------------------------------------------------------------------------------------------------------------------------------------- + # gitaly_cluster-3aff1f2bd14e6c98 | 23c4422629234d62b62adacafd0a33a8364e8619 | 23c4422629234d62b62adacafd0a33a8364e8619 | 23c4422629234d62b62adacafd0a33a8364e8619 + # + break line if line.start_with?('gitaly_cluster') + break nil if line.include?('Something went wrong when getting replicas') + end + next false unless replicas + + # We want to know if the checksums are identical + replicas&.split('|')&.map(&:strip)&.slice(1..3)&.uniq&.one? end end + def start_primary_node + start_node(@primary_node) + end + def start_praefect start_node(@praefect) end @@ -40,6 +58,14 @@ module QA stop_node(@praefect) end + def stop_secondary_node + stop_node(@secondary_node) + end + + def start_secondary_node + start_node(@secondary_node) + end + def start_node(name) shell "docker start #{name}" end @@ -49,40 +75,79 @@ module QA end def trigger_failover_by_stopping_primary_node + QA::Runtime::Logger.info("Stopping node #{@primary_node} to trigger failover") stop_node(@primary_node) + wait_for_new_primary end def clear_replication_queue - QA::Runtime::Logger.debug("Clearing the replication queue") - shell <<~CMD - docker exec --env PGPASSWORD=SQL_PASSWORD #{@postgres} \ - bash -c "psql -U postgres -d praefect_production -h postgres.test \ - -c \\"delete from replication_queue_job_lock; delete from replication_queue_lock; delete from replication_queue;\\"" - CMD + QA::Runtime::Logger.info("Clearing the replication queue") + shell sql_to_docker_exec_cmd( + <<~SQL + delete from replication_queue_job_lock; + delete from replication_queue_lock; + delete from replication_queue; + SQL + ) end def create_stalled_replication_queue - QA::Runtime::Logger.debug("Setting jobs in replication queue to `in_progress` and acquiring locks") - shell <<~CMD - docker exec --env PGPASSWORD=SQL_PASSWORD #{@postgres} \ - bash -c "psql -U postgres -d praefect_production -h postgres.test \ - -c \\"update replication_queue set state = 'in_progress'; - insert into replication_queue_job_lock (job_id, lock_id, triggered_at) - select id, rq.lock_id, created_at from replication_queue rq - left join replication_queue_job_lock rqjl on rq.id = rqjl.job_id - where state = 'in_progress' and rqjl.job_id is null; - update replication_queue_lock set acquired = 't';\\"" - CMD + QA::Runtime::Logger.info("Setting jobs in replication queue to `in_progress` and acquiring locks") + shell sql_to_docker_exec_cmd( + <<~SQL + update replication_queue set state = 'in_progress'; + insert into replication_queue_job_lock (job_id, lock_id, triggered_at) + select id, rq.lock_id, created_at from replication_queue rq + left join replication_queue_job_lock rqjl on rq.id = rqjl.job_id + where state = 'in_progress' and rqjl.job_id is null; + update replication_queue_lock set acquired = 't'; + SQL + ) + end + + # Reconciles the previous primary node with the current one + # I.e., it brings the previous primary node up-to-date + def reconcile_nodes + reconcile_node_with_node(@primary_node, current_primary_node) + end + + def reconcile_node_with_node(target, reference) + QA::Runtime::Logger.info("Reconcile #{target} with #{reference} on #{@virtual_storage}") + wait_until_shell_command_matches( + "docker exec #{@praefect} bash -c '/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml reconcile -virtual #{@virtual_storage} -target #{target} -reference #{reference} -f'", + /FINISHED: \d+ repos were checked for consistency/, + sleep_interval: 5, + retry_on_exception: true + ) + end + + def query_read_distribution + output = shell "docker exec #{@gitlab} bash -c 'curl -s http://localhost:9090/api/v1/query?query=gitaly_praefect_read_distribution'" do |line| + QA::Runtime::Logger.debug(line) + break line + end + result = JSON.parse(output) + + raise PrometheusQueryError, "Unable to query read distribution metrics" unless result['status'] == 'success' + + result['data']['result'].map { |result| { node: result['metric']['storage'], value: result['value'][1].to_i } } + end + + def replication_queue_incomplete_count + result = [] + shell sql_to_docker_exec_cmd("select count(*) from replication_queue where state = 'ready' or state = 'in_progress';") do |line| + result << line + end + # The result looks like: + # count + # ----- + # 1 + result[2].to_i end def replication_queue_lock_count result = [] - cmd = <<~CMD - docker exec --env PGPASSWORD=SQL_PASSWORD #{@postgres} \ - bash -c "psql -U postgres -d praefect_production -h postgres.test \ - -c \\"select count(*) from replication_queue_lock where acquired = 't';\\"" - CMD - shell cmd do |line| + shell sql_to_docker_exec_cmd("select count(*) from replication_queue_lock where acquired = 't';") do |line| result << line end # The result looks like: @@ -92,19 +157,65 @@ module QA result[2].to_i end - def reset_cluster - start_node(@praefect) + # Makes the original primary (gitaly1) the primary again by + # stopping the other nodes, waiting for gitaly1 to be made the + # primary again, and then it starts the other nodes and enables + # writes + def reset_primary_to_original + QA::Runtime::Logger.info("Checking primary node...") + + return if @primary_node == current_primary_node + + QA::Runtime::Logger.info("Reset primary node to #{@primary_node}") start_node(@primary_node) + stop_node(@secondary_node) + stop_node(@tertiary_node) + + wait_for_new_primary_node(@primary_node) + start_node(@secondary_node) start_node(@tertiary_node) - enable_writes + + wait_for_health_check_all_nodes + wait_for_reliable_connection + end + + def verify_storage_move(source_storage, destination_storage) + return if QA::Runtime::Env.dot_com? + + repo_path = verify_storage_move_from_gitaly(source_storage[:name]) + + destination_storage[:type] == :praefect ? verify_storage_move_to_praefect(repo_path, destination_storage[:name]) : verify_storage_move_to_gitaly(repo_path, destination_storage[:name]) end def wait_for_praefect + QA::Runtime::Logger.info('Wait until Praefect starts and is listening') wait_until_shell_command_matches( "docker exec #{@praefect} bash -c 'cat /var/log/gitlab/praefect/current'", /listening at tcp address/ ) + + # Praefect can fail to start if unable to dial one of the gitaly nodes + # See https://gitlab.com/gitlab-org/gitaly/-/issues/2847 + # We tail the logs to allow us to confirm if that is the problem if tests fail + + shell "docker exec #{@praefect} bash -c 'tail /var/log/gitlab/praefect/current'" do |line| + QA::Runtime::Logger.debug(line.chomp) + end + end + + def wait_for_new_primary_node(node) + QA::Runtime::Logger.info("Wait until #{node} is the primary node") + with_praefect_log do |log| + break true if log['msg'] == 'primary node changed' && log['newPrimary'] == node + end + end + + def wait_for_new_primary + QA::Runtime::Logger.info("Wait until a new primary node is selected") + with_praefect_log do |log| + break true if log['msg'] == 'primary node changed' + end end def wait_for_sql_ping @@ -114,68 +225,187 @@ module QA ) end + def wait_for_no_praefect_storage_error + # If a healthcheck error was the last message to be logged, we'll keep seeing that message even if it's no longer a problem + # That is, there's no message shown in the Praefect logs when the healthcheck succeeds + # To work around that we perform the gitaly check rake task, wait a few seconds, and then we confirm that no healthcheck errors appear + + QA::Runtime::Logger.info("Checking that Praefect does not report healthcheck errors with its gitaly nodes") + + Support::Waiter.wait_until(max_duration: 120) do + wait_for_gitaly_check + + sleep 5 + + shell "docker exec #{@praefect} bash -c 'tail -n 1 /var/log/gitlab/praefect/current'" do |line| + QA::Runtime::Logger.debug(line.chomp) + log = JSON.parse(line) + + break true if log['msg'] != 'error when pinging healthcheck' + rescue JSON::ParserError + # Ignore lines that can't be parsed as JSON + end + end + end + def wait_for_storage_nodes - nodes_confirmed = { - @primary_node => false, - @secondary_node => false, - @tertiary_node => false - } + wait_for_no_praefect_storage_error + + Support::Waiter.repeat_until(max_attempts: 3) do + nodes_confirmed = { + @primary_node => false, + @secondary_node => false, + @tertiary_node => false + } + + wait_until_shell_command("docker exec #{@praefect} bash -c '/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dial-nodes'") do |line| + QA::Runtime::Logger.debug(line.chomp) - wait_until_shell_command("docker exec #{@praefect} bash -c '/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dial-nodes'") do |line| - QA::Runtime::Logger.info(line.chomp) + nodes_confirmed.each_key do |node| + nodes_confirmed[node] = true if line =~ /SUCCESS: confirmed Gitaly storage "#{node}" in virtual storages \[#{@virtual_storage}\] is served/ + end - nodes_confirmed.each_key do |node| - nodes_confirmed[node] = true if line =~ /SUCCESS: confirmed Gitaly storage "#{node}" in virtual storages \[#{@virtual_storage}\] is served/ + nodes_confirmed.values.all? end + end + end + + def wait_for_health_check_current_primary_node + wait_for_health_check(current_primary_node) + end + + def wait_for_health_check_all_nodes + wait_for_health_check(@primary_node) + wait_for_health_check(@secondary_node) + wait_for_health_check(@tertiary_node) + end + + def wait_for_health_check(node) + QA::Runtime::Logger.info("Waiting for health check on #{node}") + wait_until_shell_command("docker exec #{node} bash -c 'cat /var/log/gitlab/gitaly/current'") do |line| + QA::Runtime::Logger.debug(line.chomp) + log = JSON.parse(line) + + log['grpc.request.fullMethod'] == '/grpc.health.v1.Health/Check' && log['grpc.code'] == 'OK' + rescue JSON::ParserError + # Ignore lines that can't be parsed as JSON + end + end + + def wait_for_secondary_node_health_check_failure + wait_for_health_check_failure(@secondary_node) + end + + def wait_for_health_check_failure(node) + QA::Runtime::Logger.info("Waiting for Praefect to record a health check failure on #{node}") + wait_until_shell_command("docker exec #{@praefect} bash -c 'tail -n 1 /var/log/gitlab/praefect/current'") do |line| + QA::Runtime::Logger.debug(line.chomp) + log = JSON.parse(line) - nodes_confirmed.values.all? + log['msg'] == 'error when pinging healthcheck' && log['storage'] == node + rescue JSON::ParserError + # Ignore lines that can't be parsed as JSON end end def wait_for_gitaly_check - storage_ok = false - check_finished = false + Support::Waiter.repeat_until(max_attempts: 3) do + storage_ok = false + check_finished = false - wait_until_shell_command("docker exec #{@gitlab} bash -c 'gitlab-rake gitlab:gitaly:check'") do |line| - QA::Runtime::Logger.info(line.chomp) + wait_until_shell_command("docker exec #{@gitlab} bash -c 'gitlab-rake gitlab:gitaly:check'") do |line| + QA::Runtime::Logger.debug(line.chomp) - storage_ok = true if line =~ /Gitaly: ... #{@virtual_storage} ... OK/ - check_finished = true if line =~ /Checking Gitaly ... Finished/ + storage_ok = true if line =~ /Gitaly: ... #{@virtual_storage} ... OK/ + check_finished = true if line =~ /Checking Gitaly ... Finished/ - storage_ok && check_finished + storage_ok && check_finished + end end end - def wait_for_gitlab_shell_check - wait_until_shell_command_matches( - "docker exec #{@gitlab} bash -c 'gitlab-rake gitlab:gitlab_shell:check'", - /Checking GitLab Shell ... Finished/ - ) + # Waits until there is an increase in the number of reads for + # any node compared to the number of reads provided. If a node + # has no pre-read data, consider it to have had zero reads. + def wait_for_read_count_change(pre_read_data) + diff_found = false + Support::Waiter.wait_until(sleep_interval: 5) do + query_read_distribution.each_with_index do |data, index| + diff_found = true if data[:value] > value_for_node(pre_read_data, data[:node]) + end + diff_found + end + end + + def value_for_node(data, node) + data.find(-> {{ value: 0 }}) { |item| item[:node] == node }[:value] end def wait_for_reliable_connection + QA::Runtime::Logger.info('Wait until GitLab and Praefect can communicate reliably') wait_for_praefect wait_for_sql_ping wait_for_storage_nodes wait_for_gitaly_check - wait_for_gitlab_shell_check + end + + def wait_for_replication(project_id) + Support::Waiter.wait_until(sleep_interval: 1) { replication_queue_incomplete_count == 0 && replicated?(project_id) } end private - def wait_until_shell_command(cmd) - Support::Waiter.wait_until do - shell cmd do |line| - break true if yield line - end + def current_primary_node + shell dataloss_command do |line| + QA::Runtime::Logger.debug(line.chomp) + + match = line.match(/Primary: (.*)/) + break match[1] if match + end + end + + def dataloss_command + "docker exec #{@praefect} bash -c '/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml dataloss'" + end + + def sql_to_docker_exec_cmd(sql) + Service::Shellout.sql_to_docker_exec_cmd(sql, 'postgres', 'SQL_PASSWORD', 'praefect_production', 'postgres.test', @postgres) + end + + def verify_storage_move_from_gitaly(storage) + wait_until_shell_command("docker exec #{@gitlab} bash -c 'tail -n 50 /var/log/gitlab/gitaly/current'") do |line| + log = JSON.parse(line) + + break log['grpc.request.repoPath'] if log['grpc.method'] == 'RenameRepository' && log['grpc.request.repoStorage'] == storage && !log['grpc.request.repoPath'].include?('wiki') + rescue JSON::ParserError + # Ignore lines that can't be parsed as JSON end end - def wait_until_shell_command_matches(cmd, regex) - wait_until_shell_command(cmd) do |line| - QA::Runtime::Logger.info(line.chomp) + def verify_storage_move_to_praefect(repo_path, virtual_storage) + wait_until_shell_command("docker exec #{@gitlab} bash -c 'tail -n 50 /var/log/gitlab/praefect/current'") do |line| + log = JSON.parse(line) + + log['grpc.method'] == 'ReplicateRepository' && log['virtual_storage'] == virtual_storage && log['relative_path'] == repo_path + rescue JSON::ParserError + # Ignore lines that can't be parsed as JSON + end + end + + def verify_storage_move_to_gitaly(repo_path, storage) + wait_until_shell_command("docker exec #{@gitlab} bash -c 'tail -n 50 /var/log/gitlab/gitaly/current'") do |line| + log = JSON.parse(line) + + log['grpc.method'] == 'ReplicateRepository' && log['grpc.request.repoStorage'] == storage && log['grpc.request.repoPath'] == repo_path + rescue JSON::ParserError + # Ignore lines that can't be parsed as JSON + end + end - line =~ regex + def with_praefect_log + wait_until_shell_command("docker exec #{@praefect} bash -c 'tail -n 1 /var/log/gitlab/praefect/current'") do |line| + QA::Runtime::Logger.debug(line.chomp) + yield JSON.parse(line) end end end diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb index 6efe50c4ae2..81cfaa125a9 100644 --- a/qa/qa/service/shellout.rb +++ b/qa/qa/service/shellout.rb @@ -33,6 +33,31 @@ module QA end end end + + def sql_to_docker_exec_cmd(sql, username, password, database, host, container) + <<~CMD + docker exec --env PGPASSWORD=#{password} #{container} \ + bash -c "psql -U #{username} -d #{database} -h #{host} -c \\"#{sql}\\"" + CMD + end + + def wait_until_shell_command(cmd, **kwargs) + sleep_interval = kwargs.delete(:sleep_interval) || 1 + + Support::Waiter.wait_until(sleep_interval: sleep_interval, **kwargs) do + shell cmd do |line| + break true if yield line + end + end + end + + def wait_until_shell_command_matches(cmd, regex, **kwargs) + wait_until_shell_command(cmd, kwargs) do |line| + QA::Runtime::Logger.debug(line.chomp) + + line =~ regex + end + end end end end diff --git a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb new file mode 100644 index 00000000000..064f5280625 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + context 'Gitaly automatic failover and manual recovery', :orchestrated, :gitaly_cluster, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238953', type: :flaky } do + # Variables shared between contexts. They're used and shared between + # contexts so they can't be `let` variables. + praefect_manager = Service::PraefectManager.new + project = nil + + let(:intial_commit_message) { 'Initial commit' } + let(:first_added_commit_message) { 'pushed to primary gitaly node' } + let(:second_added_commit_message) { 'commit to failover node' } + + before(:context) do + # Reset the cluster in case previous tests left it in a bad state + praefect_manager.reset_primary_to_original + + project = Resource::Project.fabricate! do |project| + project.name = "gitaly_cluster" + project.initialize_with_readme = true + end + end + + after(:context) do + # Leave the cluster in a suitable state for subsequent tests, + # if there was a problem during the tests here + praefect_manager.reset_primary_to_original + end + + it 'automatically fails over' do + # Create a new project with a commit and wait for it to replicate + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.commit_message = first_added_commit_message + push.new_branch = false + push.file_content = "This should exist on both nodes" + end + + praefect_manager.wait_for_replication(project.id) + + # Stop the primary node to trigger failover, and then wait + # for Gitaly to be ready for writes again + praefect_manager.trigger_failover_by_stopping_primary_node + praefect_manager.wait_for_new_primary + praefect_manager.wait_for_health_check_current_primary_node + praefect_manager.wait_for_gitaly_check + + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = second_added_commit_message + commit.add_files([ + { + file_path: "file-#{SecureRandom.hex(8)}", + content: 'This should exist on one node before reconciliation' + } + ]) + end + + # Confirm that we have access to the repo after failover, + # including the commit we just added + expect(project.commits.map { |commit| commit[:message].chomp }) + .to include(intial_commit_message) + .and include(first_added_commit_message) + .and include(second_added_commit_message) + end + + context 'when recovering from dataloss after failover' do + it 'allows reconciliation', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238187', type: :stale } do + # Start the old primary node again + praefect_manager.start_primary_node + praefect_manager.wait_for_health_check_current_primary_node + + # Confirm dataloss (i.e., inconsistent nodes) + expect(praefect_manager.replicated?(project.id)).to be false + + # Reconcile nodes to recover from dataloss + praefect_manager.reconcile_nodes + praefect_manager.wait_for_replication(project.id) + + # Confirm that all commits are available after reconciliation + expect(project.commits.map { |commit| commit[:message].chomp }) + .to include(intial_commit_message) + .and include(first_added_commit_message) + .and include(second_added_commit_message) + + # Restore the original primary node + praefect_manager.reset_primary_to_original + + # Check that all commits are still available even though the primary + # node was offline when one was made + expect(project.commits.map { |commit| commit[:message].chomp }) + .to include(intial_commit_message) + .and include(first_added_commit_message) + .and include(second_added_commit_message) + end + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb new file mode 100644 index 00000000000..52674f08e15 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + context 'Gitaly' do + describe 'Backend node recovery', :orchestrated, :gitaly_cluster, :skip_live_env do + let(:praefect_manager) { Service::PraefectManager.new } + let(:project) do + Resource::Project.fabricate! do |project| + project.name = "gitaly_cluster" + project.initialize_with_readme = true + end + end + + before do + # Reset the cluster in case previous tests left it in a bad state + praefect_manager.reset_primary_to_original + end + + after do + # Leave the cluster in a suitable state for subsequent tests + praefect_manager.reset_primary_to_original + end + + it 'recovers from dataloss', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238186', type: :investigating } do + # Create a new project with a commit and wait for it to replicate + praefect_manager.wait_for_replication(project.id) + + # Stop the primary node to trigger failover, and then wait + # for Gitaly to be ready for writes again + praefect_manager.trigger_failover_by_stopping_primary_node + praefect_manager.wait_for_new_primary + praefect_manager.wait_for_health_check_current_primary_node + praefect_manager.wait_for_gitaly_check + + # Confirm that we have access to the repo after failover + Support::Waiter.wait_until(retry_on_exception: true, sleep_interval: 5) do + Resource::Repository::Commit.fabricate_via_api! do |commits| + commits.project = project + commits.sha = 'master' + end + end + + # Push a commit to the new primary + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.new_branch = false + push.commit_message = 'pushed after failover' + push.file_name = 'new_file' + push.file_content = 'new file' + end + + # Start the old primary node again + praefect_manager.start_primary_node + praefect_manager.wait_for_health_check_current_primary_node + + # Confirm dataloss (i.e., inconsistent nodes) + expect(praefect_manager.replicated?(project.id)).to be false + + # Reconcile nodes to recover from dataloss + praefect_manager.reconcile_nodes + praefect_manager.wait_for_replication(project.id) + + # Confirm that both commits are available after reconciliation + expect(project.commits.map { |commit| commit[:message].chomp }) + .to include("Initial commit").and include("pushed after failover") + end + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/repository/changing_repository_storage_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb index 11e7db5b097..432598d1cb3 100644 --- a/qa/qa/specs/features/api/3_create/repository/changing_repository_storage_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb @@ -2,14 +2,15 @@ module QA RSpec.describe 'Create' do - describe 'Changing Gitaly repository storage', :requires_admin do + describe 'Changing Gitaly repository storage', :requires_admin, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/236195', type: :investigating } do + praefect_manager = Service::PraefectManager.new + praefect_manager.gitlab = 'gitlab' + shared_examples 'repository storage move' do it 'confirms a `finished` status after moving project repository storage' do expect(project).to have_file('README.md') - - project.change_repository_storage(destination_storage) - - expect(Runtime::API::RepositoryStorageMoves).to have_status(project, 'finished', destination_storage) + expect { project.change_repository_storage(destination_storage[:name]) }.not_to raise_error + expect { praefect_manager.verify_storage_move(source_storage, destination_storage) }.not_to raise_error Resource::Repository::ProjectPush.fabricate! do |push| push.project = project @@ -25,28 +26,35 @@ module QA end context 'when moving from one Gitaly storage to another', :orchestrated, :repository_storage do + let(:source_storage) { { type: :gitaly, name: 'default' } } + let(:destination_storage) { { type: :gitaly, name: QA::Runtime::Env.additional_repository_storage } } + let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'repo-storage-move-status' project.initialize_with_readme = true + project.api_client = Runtime::API::Client.as_admin end end - let(:destination_storage) { QA::Runtime::Env.additional_repository_storage } 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. - context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/227127', type: :investigating } do + # It also runs on staging using nfs-file07 as non-cluster storage and nfs-file22 as cluster/praefect storage + context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect do + let(:source_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } } + let(:destination_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } } + let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'repo-storage-move' project.initialize_with_readme = true - project.repository_storage = 'gitaly' + project.repository_storage = source_storage[:name] + project.api_client = Runtime::API::Client.as_admin end end - let(:destination_storage) { QA::Runtime::Env.praefect_repository_storage } it_behaves_like 'repository storage move' 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 new file mode 100644 index 00000000000..6292ca821ca --- /dev/null +++ b/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb @@ -0,0 +1,99 @@ +# frozen_string_literal: true + +require 'parallel' + +module QA + RSpec.describe 'Create' do + context 'Gitaly' do + # Issue to track removal of feature flag: https://gitlab.com/gitlab-org/quality/team-tasks/-/issues/602 + describe 'Distributed reads', :orchestrated, :gitaly_cluster, :skip_live_env, :requires_admin do + let(:number_of_reads_per_loop) { 9 } + let(:praefect_manager) { Service::PraefectManager.new } + let(:project) do + Resource::Project.fabricate! do |project| + project.name = "gitaly_cluster" + project.initialize_with_readme = true + end + end + + before do + Runtime::Feature.enable_and_verify('gitaly_distributed_reads') + praefect_manager.wait_for_replication(project.id) + end + + after do + Runtime::Feature.disable_and_verify('gitaly_distributed_reads') + end + + it 'reads from each node' do + pre_read_data = praefect_manager.query_read_distribution + + wait_for_reads_to_increase(project, number_of_reads_per_loop, pre_read_data) + + aggregate_failures "each gitaly node" do + praefect_manager.query_read_distribution.each_with_index do |data, index| + pre_read_count = praefect_manager.value_for_node(pre_read_data, data[:node]) + QA::Runtime::Logger.debug("Node: #{data[:node]}; before: #{pre_read_count}; now: #{data[:value]}") + expect(data[:value]).to be > pre_read_count, + "Read counts did not differ for node #{data[:node]}" + end + end + end + + context 'when a node is unhealthy' do + before do + praefect_manager.stop_secondary_node + 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_secondary_node + praefect_manager.wait_for_health_check_all_nodes + praefect_manager.wait_for_reliable_connection + end + + it 'does not read from the unhealthy node' do + pre_read_data = praefect_manager.query_read_distribution + + read_from_project(project, number_of_reads_per_loop * 10) + + praefect_manager.wait_for_read_count_change(pre_read_data) + + post_read_data = praefect_manager.query_read_distribution + + aggregate_failures "each gitaly node" do + expect(praefect_manager.value_for_node(post_read_data, 'gitaly1')).to be > praefect_manager.value_for_node(pre_read_data, 'gitaly1') + expect(praefect_manager.value_for_node(post_read_data, 'gitaly2')).to eq praefect_manager.value_for_node(pre_read_data, 'gitaly2') + expect(praefect_manager.value_for_node(post_read_data, 'gitaly3')).to be > praefect_manager.value_for_node(pre_read_data, 'gitaly3') + end + end + end + + def read_from_project(project, number_of_reads) + QA::Runtime::Logger.info('Reading from the repository') + Parallel.each((1..number_of_reads)) do + Git::Repository.perform do |repository| + repository.uri = project.repository_http_location.uri + repository.use_default_credentials + repository.clone + end + end + end + + def wait_for_reads_to_increase(project, number_of_reads, pre_read_data) + diff_found = pre_read_data + + Support::Waiter.wait_until(sleep_interval: 5, raise_on_failure: false) do + read_from_project(project, number_of_reads) + + praefect_manager.query_read_distribution.each_with_index do |data, index| + diff_found[index][:diff] = true if data[:value] > praefect_manager.value_for_node(pre_read_data, data[:node]) + end + diff_found.all? { |node| node.key?(:diff) && node[:diff] } + end + end + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/repository/praefect_replication_queue_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb index a4040a46b84..78c8639a029 100644 --- a/qa/qa/specs/features/api/3_create/repository/praefect_replication_queue_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb @@ -4,7 +4,7 @@ require 'parallel' module QA RSpec.describe 'Create' do - context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_ha, :skip_live_env do + context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do let(:praefect_manager) { Service::PraefectManager.new } let(:project) do Resource::Project.fabricate! do |project| @@ -14,7 +14,8 @@ module QA end after do - praefect_manager.reset_cluster + praefect_manager.start_praefect + praefect_manager.wait_for_reliable_connection praefect_manager.clear_replication_queue end diff --git a/qa/qa/specs/features/api/4_verify/pipeline_deletion_spec.rb b/qa/qa/specs/features/api/4_verify/pipeline_deletion_spec.rb index a406fa409d5..567815858f3 100644 --- a/qa/qa/specs/features/api/4_verify/pipeline_deletion_spec.rb +++ b/qa/qa/specs/features/api/4_verify/pipeline_deletion_spec.rb @@ -48,6 +48,12 @@ module QA let(:pipeline_data_request) { Runtime::API::Request.new(api_client, "/projects/#{project.id}/pipelines/#{pipeline_id}") } + before do + Support::Waiter.wait_until(max_duration: 30, sleep_interval: 3) do + JSON.parse(get(pipeline_data_request.url))['status'] != 'pending' + end + end + after do runner.remove_via_api! end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb new file mode 100644 index 00000000000..e83aed18b5f --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +module QA + context 'Manage', :requires_admin, :skip_live_env do + describe '2FA' do + let(:owner_user) do + Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_2fa_owner_username_1, Runtime::Env.gitlab_qa_2fa_owner_password_1) + end + + let(:developer_user) do + Resource::User.fabricate_via_api! do |resource| + resource.api_client = admin_api_client + end + end + + let(:sandbox_group) do + Resource::Sandbox.fabricate! do |sandbox_group| + sandbox_group.path = "gitlab-qa-2fa-recovery-sandbox-group-#{SecureRandom.hex(4)}" + sandbox_group.api_client = owner_api_client + end + end + + let(:group) do + QA::Resource::Group.fabricate_via_api! do |group| + group.sandbox = sandbox_group + group.api_client = owner_api_client + group.require_two_factor_authentication = true + end + end + + before do + group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER) + end + + it 'allows using 2FA recovery code once only' do + recovery_code = enable_2fa_for_user_and_fetch_recovery_code(developer_user) + + Flow::Login.sign_in(as: developer_user, skip_page_validation: true) + + Page::Main::TwoFactorAuth.perform do |two_fa_auth| + two_fa_auth.set_2fa_code(recovery_code) + two_fa_auth.click_verify_code_button + end + + expect(Page::Main::Menu.perform(&:signed_in?)).to be_truthy + + Page::Main::Menu.perform(&:sign_out) + + Flow::Login.sign_in(as: developer_user, skip_page_validation: true) + + Page::Main::TwoFactorAuth.perform do |two_fa_auth| + two_fa_auth.set_2fa_code(recovery_code) + two_fa_auth.click_verify_code_button + end + + expect(page).to have_text('Invalid two-factor code') + end + + after do + group.set_require_two_factor_authentication(value: 'false') + group.remove_via_api! + sandbox_group.remove_via_api! + developer_user.remove_via_api! + end + + def admin_api_client + @admin_api_client ||= Runtime::API::Client.as_admin + end + + def owner_api_client + @owner_api_client ||= Runtime::API::Client.new(:gitlab, user: owner_user) + end + + def enable_2fa_for_user_and_fetch_recovery_code(user) + Flow::Login.while_signed_in(as: user) do + Page::Profile::TwoFactorAuth.perform do |two_fa_auth| + @otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content) + + two_fa_auth.set_pin_code(@otp.fresh_otp) + two_fa_auth.click_register_2fa_app_button + + recovery_code = two_fa_auth.recovery_codes.sample + + two_fa_auth.click_proceed_button + + recovery_code + end + end + end + end + end +end 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 9dfeec37869..bb01be9d86e 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 'user registers and logs in' do + it 'allows the user to registers and login' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Resource::User.fabricate_via_browser_ui! @@ -16,6 +16,50 @@ module QA RSpec.describe 'Manage', :skip_signup_disabled do describe 'standard' do it_behaves_like 'registration and login' + + context 'when user account is deleted', :requires_admin do + let(:user) do + Resource::User.fabricate_via_api! do |resource| + resource.api_client = admin_api_client + end + end + + before do + # Use the UI instead of API to delete the account since + # this is the only test that exercise this UI. + # Other tests should use the API for this purpose. + Flow::Login.sign_in(as: user) + Page::Main::Menu.perform(&:click_settings_link) + Page::Profile::Menu.perform(&:click_account) + Page::Profile::Accounts::Show.perform do |show| + show.delete_account(user.password) + end + end + + it 'allows recreating with same credentials' do + expect(Page::Main::Menu.perform(&:signed_in?)).to be_falsy + + Flow::Login.sign_in(as: user, skip_page_validation: true) + + expect(page).to have_text("Invalid Login or password") + + @recreated_user = Resource::User.fabricate_via_browser_ui! do |resource| + resource.name = user.name + resource.username = user.username + resource.email = user.email + end + + expect(Page::Main::Menu.perform(&:signed_in?)).to be_truthy + end + + after do + @recreated_user.remove_via_api! + end + + def admin_api_client + @admin_api_client ||= Runtime::API::Client.as_admin + end + end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb index 3717bc8a9ff..a334731386a 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb @@ -26,7 +26,7 @@ module QA mailhog_items = mailhog_json.dig('items') - expect(mailhog_items).to include(an_object_satisfying { |o| /project was granted/ === o.dig('Content', 'Headers', 'Subject', 0) }) + expect(mailhog_items).to include(an_object_satisfying { |o| /project was granted/ === mailhog_item_subject(o) }) end private @@ -38,11 +38,22 @@ module QA mailhog_response = get QA::Runtime::MailHog.api_messages_url mailhog_data = JSON.parse(mailhog_response.body) + total = mailhog_data.dig('total') + subjects = mailhog_data.dig('items') + .map(&method(:mailhog_item_subject)) + .join("\n") + + Runtime::Logger.debug(%Q[Total number of emails: #{total}]) + Runtime::Logger.debug(%Q[Subjects:\n#{subjects}]) # Expect at least two invitation messages: group and project - mailhog_data if mailhog_data.dig('total') >= 2 + mailhog_data if total >= 2 end end + + def mailhog_item_subject(item) + item.dig('Content', 'Headers', 'Subject', 0) + end end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index e41024e5d14..91fd2579fcd 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 @@ -19,7 +19,7 @@ module QA end end - it 'closes an issue' do + it 'closes an issue', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/225303', type: :bug } do closed_issue.visit! Page::Project::Issue::Show.perform do |issue_page| diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb index 50df1c3ef01..b0b2a83ae35 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb @@ -3,17 +3,18 @@ module QA RSpec.describe 'Plan', :smoke, :reliable do describe 'mention' do - before do - Flow::Login.sign_in - - @user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) - - project = Resource::Project.fabricate_via_api! do |project| + let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } + let(:project) do + Resource::Project.fabricate_via_api! do |project| project.name = 'project-to-test-mention' project.visibility = 'private' end + end + + before do + Flow::Login.sign_in - project.add_member(@user) + project.add_member(user) Resource::Issue.fabricate_via_api! do |issue| issue.project = project @@ -22,7 +23,7 @@ module QA it 'mentions another user in an issue' do Page::Project::Issue::Show.perform do |show| - at_username = "@#{@user.username}" + at_username = "@#{user.username}" show.select_all_activities_filter show.comment(at_username) diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb index b50edcfcf08..44c1511fffb 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do - describe 'Design management' do + RSpec.describe 'Create' do + context 'Design Management' do let(:issue) { Resource::Issue.fabricate_via_api! } let(:design_filename) { 'banana_sample.gif' } let(:design) { File.absolute_path(File.join('spec', 'fixtures', design_filename)) } @@ -12,18 +12,15 @@ module QA Flow::Login.sign_in end - it 'user adds a design and annotation' do + it 'user adds a design and annotates it' do issue.visit! - Page::Project::Issue::Show.perform do |show| - show.click_designs_tab - show.add_design(design) - show.click_design(design_filename) - show.add_annotation(annotation) + Page::Project::Issue::Show.perform do |issue| + issue.add_design(design) + issue.click_design(design_filename) + issue.add_annotation(annotation) - expect(show).to have_annotation(annotation) - - show.click_discussion_tab + expect(issue).to have_annotation(annotation) end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb b/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb deleted file mode 100644 index 97a76c1aa01..00000000000 --- a/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb +++ /dev/null @@ -1,65 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Create' do - context 'Gitaly' do - describe 'High Availability', :orchestrated, :gitaly_ha do - let(:project) do - Resource::Project.fabricate! do |project| - project.name = 'gitaly_high_availability' - end - end - let(:initial_file) { 'pushed_to_primary.txt' } - let(:final_file) { 'committed_to_primary.txt' } - let(:praefect_manager) { Service::PraefectManager.new } - - before do - Flow::Login.sign_in - end - - after do - praefect_manager.reset_cluster - end - - it 'makes sure that automatic failover is happening' do - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = project - push.commit_message = 'pushed to primary gitaly node' - push.new_branch = true - push.file_name = initial_file - push.file_content = "This should exist on both nodes" - end - - praefect_manager.trigger_failover_by_stopping_primary_node - - project.visit! - - Page::Project::Show.perform do |show| - show.wait_until do - show.has_name?(project.name) - end - expect(show).to have_file(initial_file) - end - - praefect_manager.enable_writes - - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = project - commit.add_files([ - { - file_path: final_file, - content: 'This should exist on both nodes too' - } - ]) - end - - project.visit! - - Page::Project::Show.perform do |show| - expect(show).to have_file(final_file) - end - end - end - end - end -end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index a002779d7d9..524cc3fc8a1 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -16,7 +16,7 @@ module QA Flow::Login.sign_in end - it 'creates a basic merge request' do + it 'creates a basic merge request', :smoke do Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request| merge_request.project = project merge_request.title = merge_request_title diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb index 3c2c068dfd1..15c41581e6b 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb @@ -3,31 +3,30 @@ module QA RSpec.describe 'Create' do describe 'Download merge request patch and diff' do - before(:context) do - @merge_request = Resource::MergeRequest.fabricate_via_api! do |merge_request| + let(:merge_request) do + Resource::MergeRequest.fabricate_via_api! do |merge_request| merge_request.title = 'This is a merge request' merge_request.description = '... for downloading patches and diffs' end end - it 'views the merge request email patches' do + before do Flow::Login.sign_in + merge_request.visit! + end - @merge_request.visit! + it 'views the merge request email patches' do Page::MergeRequest::Show.perform(&:view_email_patches) expect(page.text).to start_with('From') expect(page).to have_content('Subject: [PATCH] This is a test commit') - expect(page).to have_content("diff --git a/#{@merge_request.file_name} b/#{@merge_request.file_name}") + expect(page).to have_content("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}") end it 'views the merge request plain diff' do - Flow::Login.sign_in - - @merge_request.visit! Page::MergeRequest::Show.perform(&:view_plain_diff) - expect(page.text).to start_with("diff --git a/#{@merge_request.file_name} b/#{@merge_request.file_name}") + expect(page.text).to start_with("diff --git a/#{merge_request.file_name} b/#{merge_request.file_name}") expect(page).to have_content('+File Added') end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb index f586c25165c..59e4bb038a7 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb @@ -10,6 +10,7 @@ module QA project.initialize_with_readme = true end end + let(:fork_project) do Resource::Fork.fabricate_via_api! do |fork| fork.user = user diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index c01558d3702..ceacc73e3c3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Git push over HTTP', :ldap_no_tls do + describe 'Git push over HTTP', :ldap_no_tls, :smoke do it 'user using a personal access token pushes code to the repository' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index b918b2ff268..2baf1e1d8fd 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -3,7 +3,7 @@ module QA RSpec.describe 'Create' do describe 'Git push over HTTP', :ldap_no_tls do - it 'user pushes code to the repository' do + it 'user pushes code to the repository', :smoke do Flow::Login.sign_in Resource::Repository::ProjectPush.fabricate! do |push| diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb new file mode 100644 index 00000000000..d0f0cabbbca --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'SSH key support' do + # Note: If you run these tests against GDK make sure you've enabled sshd + # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md + + let(:project) do + Resource::Project.fabricate! do |project| + project.name = 'ssh-tests' + end + end + + before(:context) do + @key = Resource::SSHKey.fabricate_via_api! do |resource| + resource.title = "key for ssh tests #{Time.now.to_f}" + end + end + + after(:context) do + @key.remove_via_api! + end + + before do + Flow::Login.sign_in + end + + it 'pushes code to the repository via SSH', :smoke do + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.ssh_key = @key + push.file_name = 'README.md' + push.file_content = '# Test Use SSH Key' + push.commit_message = 'Add README.md' + end.project.visit! + + Page::Project::Show.perform do |project| + expect(project).to have_file('README.md') + expect(project).to have_readme_content('Test Use SSH Key') + end + end + + it 'pushes multiple branches and tags together', :smoke do + branches = [] + tags = [] + Git::Repository.perform do |repository| + repository.uri = project.repository_ssh_location.uri + repository.use_ssh_key(@key) + repository.clone + repository.configure_identity('GitLab QA', 'root@gitlab.com') + 1.upto(3) do |i| + branches << "branch#{i}" + tags << "tag#{i}" + repository.checkout("branch#{i}", new_branch: true) + repository.commit_file("file#{i}", SecureRandom.random_bytes(10000), "Add file#{i}") + repository.add_tag("tag#{i}") + end + repository.push_tags_and_branches(branches) + end + + expect(project).to have_branches(branches) + expect(project).to have_tags(tags) + end + 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 d67e4a4ea83..c5a07c69620 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 @@ -24,10 +24,10 @@ module QA Page::Main::Menu.perform(&:click_settings_link) Page::Profile::Menu.perform(&:click_ssh_keys) Page::Profile::SSHKeys.perform do |ssh_keys| - ssh_keys.remove_key(key_title) + ssh_keys.remove_key(key.title) end - expect(page).not_to have_content("Title: #{key_title}") + expect(page).not_to have_content("Title: #{key.title}") expect(page).not_to have_content(key.md5_fingerprint) end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb deleted file mode 100644 index e91717b0f5f..00000000000 --- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb +++ /dev/null @@ -1,37 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Create' do - describe 'SSH key support' do - # Note: If you run this test against GDK make sure you've enabled sshd - # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md - - let(:key_title) { "key for ssh tests #{Time.now.to_f}" } - - it 'user adds an ssh key and pushes code to the repository' do - Flow::Login.sign_in - - key = Resource::SSHKey.fabricate_via_api! do |resource| - resource.title = key_title - end - - Resource::Repository::ProjectPush.fabricate! do |push| - push.ssh_key = key - push.file_name = 'README.md' - push.file_content = '# Test Use SSH Key' - push.commit_message = 'Add README.md' - end.project.visit! - - expect(page).to have_content('README.md') - expect(page).to have_content('Test Use SSH Key') - - Page::Main::Menu.perform(&:click_settings_link) - Page::Profile::Menu.perform(&:click_ssh_keys) - - Page::Profile::SSHKeys.perform do |ssh_keys| - ssh_keys.remove_key(key_title) - end - end - end - end -end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb index e6589851dd9..ddbeb434955 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb @@ -87,7 +87,7 @@ module QA repository.init_repository expect { repository.pull(repository_uri_ssh, branch_name) } - .to raise_error(QA::Git::Repository::RepositoryCommandError, /[fatal: Could not read from remote repository.]+/) + .to raise_error(QA::Git::Repository::RepositoryCommandError, /fatal: Could not read from remote repository\./) end end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb index 1660944fccd..dc1654b44c8 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb @@ -86,7 +86,7 @@ module QA repository.init_repository expect { repository.pull(repository_uri_ssh, branch_name) } - .to raise_error(QA::Git::Repository::RepositoryCommandError, /[fatal: Could not read from remote repository.]+/) + .to raise_error(QA::Git::Repository::RepositoryCommandError, /fatal: Could not read from remote repository\./) end end diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb new file mode 100644 index 00000000000..ad7455242bc --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb @@ -0,0 +1,67 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Open a fork in Web IDE' do + let(:parent_project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'parent-project' + project.initialize_with_readme = true + end + end + + context 'when a user does not have permissions to commit to the project' do + let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } + + context 'when no fork is present' do + it 'suggests to create a fork when a user clicks Web IDE in the main project' do + Flow::Login.sign_in(as: user) + + parent_project.visit! + Page::Project::Show.perform(&:open_web_ide!) + + Page::Project::WebIDE::Edit.perform(&:fork_project!) + + submit_merge_request_upstream + end + end + + context 'when a fork is already created' do + let(:fork_project) do + Resource::Fork.fabricate_via_api! do |fork| + fork.user = user + fork.upstream = parent_project + end + end + + it 'opens the fork when a user clicks Web IDE in the main project' do + Flow::Login.sign_in(as: user) + fork_project.upstream.visit! + Page::Project::Show.perform do |project_page| + expect(project_page).to have_edit_fork_button + + project_page.open_web_ide! + end + + submit_merge_request_upstream + end + end + + def submit_merge_request_upstream + Page::Project::WebIDE::Edit.perform do |ide| + expect(ide).to have_project_path("#{user.username}/#{parent_project.name}") + + ide.add_file('new file', 'some random text') + ide.commit_changes(open_merge_request: true) + end + + Page::MergeRequest::New.perform(&:create_merge_request) + + parent_project.visit! + Page::Project::Menu.perform(&:click_merge_requests) + expect(page).to have_content('Update new file') + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb new file mode 100644 index 00000000000..e0d54611731 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + context 'Wiki' do + let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! } + let(:new_path) { "a/new/path" } + + before do + Flow::Login.sign_in + end + + it 'has changed the directory' do + initial_wiki.visit! + + Page::Project::Wiki::Show.perform(&:click_edit) + + Page::Project::Wiki::Edit.perform do |edit| + edit.set_title("#{new_path}/home") + edit.set_message('changing the path of the home page') + end + + Page::Project::Wiki::Edit.perform(&:click_save_changes) + + Page::Project::Wiki::Show.perform do |wiki| + expect(wiki).to have_directory(new_path) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb index 41baaa02544..fd342503a5c 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Verify', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/229724', type: :investigating } do + RSpec.describe 'Verify' do describe 'Add or Remove CI variable via UI', :smoke do let!(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/5_package/.gitkeep b/qa/qa/specs/features/browser_ui/5_package/.gitkeep deleted file mode 100644 index e69de29bb2d..00000000000 --- a/qa/qa/specs/features/browser_ui/5_package/.gitkeep +++ /dev/null diff --git a/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb new file mode 100644 index 00000000000..0f04b3b6186 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Package', :docker, :orchestrated, :packages do + describe 'Maven Repository' do + include Runtime::Fixtures + + let(:group_id) { 'com.gitlab.qa' } + let(:artifact_id) { 'maven' } + let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') } + let(:auth_token) do + unless Page::Main::Menu.perform(&:signed_in?) + Flow::Login.sign_in + end + + Resource::PersonalAccessToken.fabricate!.access_token + end + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'maven-package-project' + end + end + + it 'publishes a maven package and deletes it' do + uri = URI.parse(Runtime::Scenario.gitlab_address) + gitlab_address_with_port = "#{uri.scheme}://#{uri.host}:#{uri.port}" + pom_xml = { + file_path: 'pom.xml', + content: <<~XML + <project> + <groupId>#{group_id}</groupId> + <artifactId>#{artifact_id}</artifactId> + <version>1.0</version> + <modelVersion>4.0.0</modelVersion> + <repositories> + <repository> + <id>#{project.name}</id> + <url>#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/maven</url> + </repository> + </repositories> + <distributionManagement> + <repository> + <id>#{project.name}</id> + <url>#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/maven</url> + </repository> + <snapshotRepository> + <id>#{project.name}</id> + <url>#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/maven</url> + </snapshotRepository> + </distributionManagement> + </project> + XML + } + settings_xml = { + file_path: 'settings.xml', + content: <<~XML + <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> + <servers> + <server> + <id>#{project.name}</id> + <configuration> + <httpHeaders> + <property> + <name>Private-Token</name> + <value>#{auth_token}</value> + </property> + </httpHeaders> + </configuration> + </server> + </servers> + </settings> + XML + } + + # Use a maven docker container to deploy the package + with_fixtures([pom_xml, settings_xml]) do |dir| + Service::DockerRun::Maven.new(dir).publish! + end + + project.visit! + Page::Project::Menu.perform(&:click_packages_link) + + Page::Project::Packages::Index.perform do |index| + expect(index).to have_package(package_name) + + index.click_package(package_name) + end + + Page::Project::Packages::Show.perform do |show| + expect(show).to have_package_info(package_name, "1.0") + + show.click_delete + end + + Page::Project::Packages::Index.perform do |index| + expect(index).to have_content("Package was removed") + expect(index).to have_no_package(package_name) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb new file mode 100644 index 00000000000..471d66c2f21 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Package', :docker, :orchestrated, :packages do + describe 'NPM registry' do + include Runtime::Fixtures + + let(:registry_scope) { project.group.sandbox.path } + let(:package_name) { "@#{registry_scope}/#{project.name}" } + let(:auth_token) do + unless Page::Main::Menu.perform(&:signed_in?) + Flow::Login.sign_in + end + + Resource::PersonalAccessToken.fabricate!.access_token + end + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'npm-registry-project' + end + end + + it 'publishes an npm package and then deletes it' do + uri = URI.parse(Runtime::Scenario.gitlab_address) + gitlab_host_with_port = "#{uri.host}:#{uri.port}" + gitlab_address_with_port = "#{uri.scheme}://#{uri.host}:#{uri.port}" + package_json = { + file_path: 'package.json', + content: <<~JSON + { + "name": "#{package_name}", + "version": "1.0.0", + "description": "Example package for GitLab NPM registry", + "publishConfig": { + "@#{registry_scope}:registry": "#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/npm/" + } + } + JSON + } + npmrc = { + file_path: '.npmrc', + content: <<~NPMRC + //#{gitlab_host_with_port}/api/v4/projects/#{project.id}/packages/npm/:_authToken=#{auth_token} + //#{gitlab_host_with_port}/api/v4/packages/npm/:_authToken=#{auth_token} + @#{registry_scope}:registry=#{gitlab_address_with_port}/api/v4/packages/npm/ + NPMRC + } + + # Use a node docker container to publish the package + with_fixtures([npmrc, package_json]) do |dir| + Service::DockerRun::NodeJs.new(dir).publish! + end + + project.visit! + Page::Project::Menu.perform(&:click_packages_link) + + Page::Project::Packages::Index.perform do |index| + expect(index).to have_package(package_name) + + index.click_package(package_name) + end + + Page::Project::Packages::Show.perform do |show| + expect(show).to have_package_info(package_name, "1.0.0") + + show.click_delete + end + + Page::Project::Packages::Index.perform do |index| + expect(index).to have_content("Package was removed") + expect(index).to have_no_package(package_name) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb index 673125c90f2..ba36e4fa290 100644 --- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb @@ -8,6 +8,7 @@ module QA project.name = 'pipelines-dependent-relationship' end end + let!(:runner) do Resource::Runner.fabricate_via_api! do |runner| runner.project = project diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb index 05b9859f112..69f66ee4edf 100644 --- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb @@ -8,6 +8,7 @@ module QA project.name = 'pipeline-independent-relationship' end end + let!(:runner) do Resource::Runner.fabricate_via_api! do |runner| runner.project = project 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 ad87ee173f5..3e25ecfd45d 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 @@ -15,7 +15,7 @@ module QA disable_optional_jobs(project) end - describe 'Auto DevOps support', :orchestrated, :kubernetes do + describe 'Auto DevOps support', :orchestrated, :kubernetes, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/230927', type: :stale } do context 'when rbac is enabled' do let(:cluster) { Service::KubernetesCluster.new.create! } @@ -38,7 +38,6 @@ module QA Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster| k8s_cluster.project = project k8s_cluster.cluster = cluster - k8s_cluster.install_helm_tiller = true k8s_cluster.install_ingress = true k8s_cluster.install_prometheus = true k8s_cluster.install_runner = true diff --git a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb index 9cfdc4277a7..54c7b75d1d1 100644 --- a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb +++ b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb @@ -2,12 +2,13 @@ module QA RSpec.describe 'Monitor' do - describe 'with Prometheus in a Gitlab-managed cluster', :orchestrated, :kubernetes do + describe 'with Prometheus in a Gitlab-managed cluster', :orchestrated, :kubernetes, :requires_admin do before :all do - @cluster = Service::KubernetesCluster.new.create! + @cluster = Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! @project = Resource::Project.fabricate_via_api! do |project| project.name = 'monitoring-project' project.auto_devops_enabled = true + project.template_name = 'express' end deploy_project_with_prometheus @@ -83,7 +84,7 @@ module QA %w[ CODE_QUALITY_DISABLED TEST_DISABLED LICENSE_MANAGEMENT_DISABLED SAST_DISABLED DAST_DISABLED DEPENDENCY_SCANNING_DISABLED - CONTAINER_SCANNING_DISABLED PERFORMANCE_DISABLED + CONTAINER_SCANNING_DISABLED PERFORMANCE_DISABLED SECRET_DETECTION_DISABLED ].each do |key| Resource::CiVariable.fabricate_via_api! do |resource| resource.project = @project @@ -98,22 +99,14 @@ module QA Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings| cluster_settings.project = @project cluster_settings.cluster = @cluster - cluster_settings.install_helm_tiller = true cluster_settings.install_runner = true cluster_settings.install_ingress = true cluster_settings.install_prometheus = true end - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = @project - push.directory = Pathname - .new(__dir__) - .join('../../../../fixtures/auto_devops_rack') - push.commit_message = 'Create AutoDevOps compatible Project for Monitoring' - end - - Page::Project::Menu.perform(&:click_ci_cd_pipelines) - Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline) + Resource::Pipeline.fabricate_via_api! do |pipeline| + pipeline.project = @project + end.visit! Page::Project::Pipeline::Show.perform do |pipeline| pipeline.click_job('build') diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 3c46c039eae..08faacb6db3 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -77,6 +77,30 @@ module QA error.response end + + def with_paginated_response_body(url) + loop do + response = get(url) + + QA::Runtime::Logger.debug("Fetching page #{response.headers[:x_page]} of #{response.headers[:x_total_pages]}...") + + yield parse_body(response) + + next_link = pagination_links(response).find { |link| link[:rel] == 'next' } + break unless next_link + + url = next_link[:url] + end + end + + def pagination_links(response) + response.headers[:link].split(',').map do |link| + match = link.match(/\<(?<url>.*)\>\; rel=\"(?<rel>\w+)\"/) + break nil unless match + + { url: match[:url], rel: match[:rel] } + end.compact + end end end end diff --git a/qa/qa/support/json_formatter.rb b/qa/qa/support/json_formatter.rb new file mode 100644 index 00000000000..5d2a3e7b75f --- /dev/null +++ b/qa/qa/support/json_formatter.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'rspec/core/formatters' + +module QA + module Support + class JsonFormatter < RSpec::Core::Formatters::JsonFormatter + RSpec::Core::Formatters.register self, :message, :dump_summary, :stop, :seed, :close + + def dump_profile(profile) + # We don't currently use the profile info. This overrides the base + # implementation so that it's not included. + end + + def stop(notification) + # Based on https://github.com/rspec/rspec-core/blob/main/lib/rspec/core/formatters/json_formatter.rb#L35 + # But modified to include full details of multiple exceptions + @output_hash[:examples] = notification.examples.map do |example| + format_example(example).tap do |hash| + e = example.exception + if e + exceptions = e.respond_to?(:all_exceptions) ? e.all_exceptions : [e] + hash[:exceptions] = exceptions.map do |exception| + { + class: exception.class.name, + message: exception.message, + backtrace: exception.backtrace + } + end + end + end + end + end + + private + + def format_example(example) + { + id: example.id, + description: example.description, + full_description: example.full_description, + status: example.execution_result.status.to_s, + file_path: example.metadata[:file_path], + line_number: example.metadata[:line_number], + run_time: example.execution_result.run_time, + pending_message: example.execution_result.pending_message, + status_issue: example.metadata[:status_issue], + quarantine: example.metadata[:quarantine], + screenshot: example.metadata[:screenshot] + } + end + end + end +end diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index 281e1b85cc3..ea0307e58b2 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -64,6 +64,12 @@ module QA super end + def click_element_coordinates(name) + log(%Q(clicking the coordinates of :#{name})) + + super + end + def click_element(name, page = nil, **kwargs) msg = ["clicking :#{name}"] msg << ", expecting to be at #{page.class}" if page @@ -120,7 +126,7 @@ module QA found end - def finished_loading? + def finished_loading?(wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME) log('waiting for loading to complete...') now = Time.now diff --git a/qa/qa/support/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb index c58882a11ea..943d7d510df 100644 --- a/qa/qa/support/wait_for_requests.rb +++ b/qa/qa/support/wait_for_requests.rb @@ -5,20 +5,32 @@ module QA module WaitForRequests module_function - def wait_for_requests + DEFAULT_MAX_WAIT_TIME = 60 + + def wait_for_requests(skip_finished_loading_check: false) Waiter.wait_until(log: false) do - finished_all_ajax_requests? && finished_all_axios_requests? + finished_all_ajax_requests? && finished_all_axios_requests? && (!skip_finished_loading_check ? finished_loading?(wait: 1) : true) end end def finished_all_axios_requests? - Capybara.page.evaluate_script('window.pendingRequests || 0').zero? + Capybara.page.evaluate_script('window.pendingRequests || 0').zero? # rubocop:disable Style/NumericPredicate end def finished_all_ajax_requests? return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"') - Capybara.page.evaluate_script('jQuery.active').zero? + Capybara.page.evaluate_script('jQuery.active').zero? # rubocop:disable Style/NumericPredicate + end + + def finished_loading?(wait: DEFAULT_MAX_WAIT_TIME) + # The number of selectors should be able to be reduced after + # migration to the new spinner is complete. + # https://gitlab.com/groups/gitlab-org/-/epics/956 + # retry_on_exception added here due to `StaleElementReferenceError`. See: https://gitlab.com/gitlab-org/gitlab/-/issues/232485 + Support::Retrier.retry_on_exception do + Capybara.page.has_no_css?('.gl-spinner, .fa-spinner, .spinner', wait: wait) + end end end end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index fe84b3d024a..b23de19e1f8 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -226,6 +226,7 @@ describe QA::Resource::Base do end end end + let(:first_resource) do Class.new(base) do attribute :test do @@ -233,6 +234,7 @@ describe QA::Resource::Base do end end end + let(:second_resource) do Class.new(base) do attribute :test do diff --git a/qa/spec/resource/events/project_spec.rb b/qa/spec/resource/events/project_spec.rb index dd544ec7ac8..98da87906fa 100644 --- a/qa/spec/resource/events/project_spec.rb +++ b/qa/spec/resource/events/project_spec.rb @@ -8,6 +8,7 @@ describe QA::Resource::Events::Project do end end end + let(:all_events) do [ { diff --git a/qa/spec/scenario/template_spec.rb b/qa/spec/scenario/template_spec.rb index f97fc22daf9..65793734548 100644 --- a/qa/spec/scenario/template_spec.rb +++ b/qa/spec/scenario/template_spec.rb @@ -17,6 +17,24 @@ describe QA::Scenario::Template do expect(feature).to have_received(:enable).with('a-feature') end + it 'allows a feature to be disabled' do + allow(QA::Runtime::Feature).to receive(:enabled?) + .with('another-feature').and_return(true) + + subject.perform({ disable_feature: 'another-feature' }) + + expect(feature).to have_received(:disable).with('another-feature') + end + + it 'does not disable a feature if already disabled' do + allow(QA::Runtime::Feature).to receive(:enabled?) + .with('another-feature').and_return(false) + + subject.perform({ disable_feature: 'another-feature' }) + + expect(feature).not_to have_received(:disable).with('another-feature') + end + it 'ensures an enabled feature is disabled afterwards' do allow(QA::Specs::Runner).to receive(:perform).and_raise('failed test') @@ -25,4 +43,28 @@ describe QA::Scenario::Template do expect(feature).to have_received(:enable).with('a-feature') expect(feature).to have_received(:disable).with('a-feature') end + + it 'ensures a disabled feature is enabled afterwards' do + allow(QA::Specs::Runner).to receive(:perform).and_raise('failed test') + + allow(QA::Runtime::Feature).to receive(:enabled?) + .with('another-feature').and_return(true) + + expect { subject.perform({ disable_feature: 'another-feature' }) }.to raise_error('failed test') + + expect(feature).to have_received(:disable).with('another-feature') + expect(feature).to have_received(:enable).with('another-feature') + end + + it 'ensures a disabled feature is not enabled afterwards if it was disabled earlier' do + allow(QA::Specs::Runner).to receive(:perform).and_raise('failed test') + + allow(QA::Runtime::Feature).to receive(:enabled?) + .with('another-feature').and_return(false) + + expect { subject.perform({ disable_feature: 'another-feature' }) }.to raise_error('failed test') + + expect(feature).not_to have_received(:disable).with('another-feature') + expect(feature).not_to have_received(:enable).with('another-feature') + end end diff --git a/qa/spec/scenario/test/integration/mattermost_spec.rb b/qa/spec/scenario/test/integration/mattermost_spec.rb index 4452e890ebe..7e4eb6284e8 100644 --- a/qa/spec/scenario/test/integration/mattermost_spec.rb +++ b/qa/spec/scenario/test/integration/mattermost_spec.rb @@ -10,6 +10,7 @@ describe QA::Scenario::Test::Integration::Mattermost do mattermost_address: 'http://mattermost_address' } end + let(:named_options) { %w[--address http://gitlab_address --mattermost-address http://mattermost_address] } let(:tags) { [:mattermost] } let(:options) { ['path1']} diff --git a/qa/spec/support/wait_for_requests_spec.rb b/qa/spec/support/wait_for_requests_spec.rb new file mode 100644 index 00000000000..79ee3eb5099 --- /dev/null +++ b/qa/spec/support/wait_for_requests_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +describe QA::Support::WaitForRequests do + describe '.wait_for_requests' do + before do + allow(subject).to receive(:finished_all_axios_requests?).and_return(true) + allow(subject).to receive(:finished_all_ajax_requests?).and_return(true) + allow(subject).to receive(:finished_loading?).and_return(true) + end + + context 'when skip_finished_loading_check is defaulted to false' do + it 'calls finished_loading?' do + expect(subject).to receive(:finished_loading?).with(hash_including(wait: 1)) + + subject.wait_for_requests + end + end + + context 'when skip_finished_loading_check is true' do + it 'does not call finished_loading?' do + expect(subject).not_to receive(:finished_loading?) + + subject.wait_for_requests(skip_finished_loading_check: true) + end + end + end +end |