summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
Diffstat (limited to 'qa')
-rw-r--r--qa/Gemfile7
-rw-r--r--qa/Gemfile.lock3
-rw-r--r--qa/README.md25
-rw-r--r--qa/Rakefile8
-rw-r--r--qa/qa.rb17
-rw-r--r--qa/qa/fixtures/auto_devops_rack/Gemfile.lock4
-rw-r--r--qa/qa/flow/saml.rb3
-rw-r--r--qa/qa/git/repository.rb10
-rw-r--r--qa/qa/page/admin/menu.rb14
-rw-r--r--qa/qa/page/admin/settings/general.rb4
-rw-r--r--qa/qa/page/admin/settings/metrics_and_profiling.rb4
-rw-r--r--qa/qa/page/admin/settings/network.rb8
-rw-r--r--qa/qa/page/admin/settings/repository.rb4
-rw-r--r--qa/qa/page/base.rb20
-rw-r--r--qa/qa/page/component/confirm_modal.rb9
-rw-r--r--qa/qa/page/component/new_snippet.rb2
-rw-r--r--qa/qa/page/component/snippet.rb12
-rw-r--r--qa/qa/page/dashboard/projects.rb11
-rw-r--r--qa/qa/page/dashboard/snippet/edit.rb3
-rw-r--r--qa/qa/page/dashboard/snippet/show.rb4
-rw-r--r--qa/qa/page/group/members.rb84
-rw-r--r--qa/qa/page/group/settings/general.rb18
-rw-r--r--qa/qa/page/group/sub_menus/members.rb51
-rw-r--r--qa/qa/page/main/menu.rb15
-rw-r--r--qa/qa/page/merge_request/new.rb2
-rw-r--r--qa/qa/page/merge_request/show.rb6
-rw-r--r--qa/qa/page/profile/accounts/show.rb30
-rw-r--r--qa/qa/page/profile/menu.rb7
-rw-r--r--qa/qa/page/profile/two_factor_auth.rb9
-rw-r--r--qa/qa/page/project/fork/new.rb4
-rw-r--r--qa/qa/page/project/issue/index.rb21
-rw-r--r--qa/qa/page/project/issue/show.rb21
-rw-r--r--qa/qa/page/project/menu.rb1
-rw-r--r--qa/qa/page/project/operations/incidents/index.rb21
-rw-r--r--qa/qa/page/project/operations/kubernetes/add_existing.rb2
-rw-r--r--qa/qa/page/project/operations/kubernetes/show.rb4
-rw-r--r--qa/qa/page/project/operations/metrics/show.rb16
-rw-r--r--qa/qa/page/project/packages/index.rb28
-rw-r--r--qa/qa/page/project/packages/show.rb27
-rw-r--r--qa/qa/page/project/settings/advanced.rb17
-rw-r--r--qa/qa/page/project/settings/ci_cd.rb8
-rw-r--r--qa/qa/page/project/settings/ci_variables.rb2
-rw-r--r--qa/qa/page/project/settings/main.rb6
-rw-r--r--qa/qa/page/project/settings/operations.rb2
-rw-r--r--qa/qa/page/project/settings/protected_branches.rb2
-rw-r--r--qa/qa/page/project/settings/repository.rb18
-rw-r--r--qa/qa/page/project/show.rb4
-rw-r--r--qa/qa/page/project/snippet/new.rb1
-rw-r--r--qa/qa/page/project/sub_menus/operations.rb9
-rw-r--r--qa/qa/page/project/sub_menus/packages.rb29
-rw-r--r--qa/qa/page/project/web_ide/edit.rb63
-rw-r--r--qa/qa/page/project/wiki/sidebar.rb8
-rw-r--r--qa/qa/page/search/results.rb4
-rw-r--r--qa/qa/page/settings/common.rb3
-rw-r--r--qa/qa/resource/group.rb5
-rw-r--r--qa/qa/resource/kubernetes_cluster/project_cluster.rb40
-rw-r--r--qa/qa/resource/members.rb7
-rw-r--r--qa/qa/resource/project.rb30
-rw-r--r--qa/qa/resource/repository/commit.rb13
-rw-r--r--qa/qa/resource/ssh_key.rb1
-rw-r--r--qa/qa/resource/user.rb2
-rw-r--r--qa/qa/runtime/api/client.rb2
-rw-r--r--qa/qa/runtime/api/repository_storage_moves.rb16
-rw-r--r--qa/qa/runtime/env.rb8
-rw-r--r--qa/qa/scenario/shared_attributes.rb1
-rw-r--r--qa/qa/scenario/template.rb3
-rw-r--r--qa/qa/scenario/test/integration/gitaly_ha.rb13
-rw-r--r--qa/qa/service/cluster_provider/k3s.rb1
-rw-r--r--qa/qa/service/docker_run/gitlab_runner.rb2
-rw-r--r--qa/qa/service/docker_run/k3s.rb6
-rw-r--r--qa/qa/service/praefect_manager.rb370
-rw-r--r--qa/qa/service/shellout.rb25
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb100
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb71
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb (renamed from qa/qa/specs/features/api/3_create/repository/changing_repository_storage_spec.rb)26
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb99
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb (renamed from qa/qa/specs/features/api/3_create/repository/praefect_replication_queue_spec.rb)5
-rw-r--r--qa/qa/specs/features/api/4_verify/pipeline_deletion_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb92
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb46
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb15
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb17
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb (renamed from qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb)19
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb65
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb17
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb67
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb37
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb67
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb31
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/.gitkeep0
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb104
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb77
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb21
-rw-r--r--qa/qa/support/api.rb24
-rw-r--r--qa/qa/support/json_formatter.rb54
-rw-r--r--qa/qa/support/page/logging.rb8
-rw-r--r--qa/qa/support/wait_for_requests.rb20
-rw-r--r--qa/spec/resource/base_spec.rb2
-rw-r--r--qa/spec/resource/events/project_spec.rb1
-rw-r--r--qa/spec/scenario/template_spec.rb42
-rw-r--r--qa/spec/scenario/test/integration/mattermost_spec.rb1
-rw-r--r--qa/spec/support/wait_for_requests_spec.rb27
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
diff --git a/qa/qa.rb b/qa/qa.rb
index 823adade6f3..c6973533144 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -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