summaryrefslogtreecommitdiff
path: root/qa
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 20:02:30 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2022-03-18 20:02:30 +0000
commit41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch)
tree9c8d89a8624828992f06d892cd2f43818ff5dcc8 /qa
parent0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff)
downloadgitlab-ce-41fe97390ceddf945f3d967b8fdb3de4c66b7dea.tar.gz
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'qa')
-rw-r--r--qa/Gemfile4
-rw-r--r--qa/Gemfile.lock66
-rw-r--r--qa/Rakefile23
-rw-r--r--qa/contracts/.gitignore2
-rw-r--r--qa/contracts/consumer/.node-version1
-rw-r--r--qa/contracts/consumer/endpoints/merge_request.js42
-rw-r--r--qa/contracts/consumer/fixtures/diffs.fixture.js89
-rw-r--r--qa/contracts/consumer/fixtures/discussions.fixture.js85
-rw-r--r--qa/contracts/consumer/fixtures/metadata.fixture.js96
-rw-r--r--qa/contracts/consumer/package.json17
-rw-r--r--qa/contracts/consumer/specs/diffs.spec.js35
-rw-r--r--qa/contracts/consumer/specs/discussions.spec.js35
-rw-r--r--qa/contracts/consumer/specs/metadata.spec.js35
-rw-r--r--qa/contracts/contracts/merge_request_page-merge_request_diffs_endpoint.json228
-rw-r--r--qa/contracts/contracts/merge_request_page-merge_request_discussions_endpoint.json235
-rw-r--r--qa/contracts/contracts/merge_request_page-merge_request_metadata_endpoint.json222
-rw-r--r--qa/contracts/provider/environments/base.rb24
-rw-r--r--qa/contracts/provider/environments/local.rb12
-rw-r--r--qa/contracts/provider/spec/diffs_helper.rb17
-rw-r--r--qa/contracts/provider/spec/discussions_helper.rb17
-rw-r--r--qa/contracts/provider/spec/metadata_helper.rb17
-rw-r--r--qa/contracts/provider/spec_helper.rb22
-rw-r--r--qa/knapsack/master_report.json1
-rw-r--r--qa/qa.rb9
-rw-r--r--qa/qa/git/location.rb1
-rw-r--r--qa/qa/page/component/new_snippet.rb5
-rw-r--r--qa/qa/page/component/snippet.rb4
-rw-r--r--qa/qa/page/dashboard/projects.rb14
-rw-r--r--qa/qa/page/dashboard/snippet/edit.rb5
-rw-r--r--qa/qa/page/dashboard/snippet/show.rb2
-rw-r--r--qa/qa/page/file/show.rb13
-rw-r--r--qa/qa/page/group/settings/general.rb10
-rw-r--r--qa/qa/page/main/login.rb5
-rw-r--r--qa/qa/page/merge_request/show.rb8
-rw-r--r--qa/qa/page/modal/delete_issue.rb17
-rw-r--r--qa/qa/page/project/fork/new.rb14
-rw-r--r--qa/qa/page/project/infrastructure/kubernetes/add.rb21
-rw-r--r--qa/qa/page/project/issue/show.rb16
-rw-r--r--qa/qa/page/project/new.rb10
-rw-r--r--qa/qa/page/project/pipeline_editor/new.rb19
-rw-r--r--qa/qa/page/project/pipeline_editor/show.rb20
-rw-r--r--qa/qa/page/project/settings/mirroring_repositories.rb11
-rw-r--r--qa/qa/page/project/show.rb2
-rw-r--r--qa/qa/page/project/web_ide/edit.rb2
-rw-r--r--qa/qa/page/trials/new.rb2
-rw-r--r--qa/qa/page/trials/select.rb11
-rw-r--r--qa/qa/resource/api_fabricator.rb19
-rw-r--r--qa/qa/resource/deploy_key.rb42
-rw-r--r--qa/qa/resource/fork.rb2
-rw-r--r--qa/qa/resource/group.rb9
-rw-r--r--qa/qa/resource/group_base.rb20
-rw-r--r--qa/qa/resource/kubernetes_cluster/project_cluster.rb3
-rw-r--r--qa/qa/resource/project.rb36
-rw-r--r--qa/qa/resource/protected_branch.rb10
-rw-r--r--qa/qa/resource/reusable_collection.rb4
-rw-r--r--qa/qa/resource/reusable_group.rb2
-rw-r--r--qa/qa/resource/reusable_project.rb2
-rw-r--r--qa/qa/resource/runner.rb5
-rw-r--r--qa/qa/runtime/allure_report.rb2
-rw-r--r--qa/qa/runtime/api/client.rb21
-rw-r--r--qa/qa/runtime/env.rb17
-rw-r--r--qa/qa/runtime/logger.rb26
-rw-r--r--qa/qa/scenario/test/integration/object_storage_gcs.rb13
-rw-r--r--qa/qa/scenario/test/integration/registry_with_cdn.rb14
-rw-r--r--qa/qa/service/praefect_manager.rb37
-rw-r--r--qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb55
-rw-r--r--qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb243
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb41
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb74
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb59
-rw-r--r--qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb16
-rw-r--r--qa/qa/specs/features/api/1_manage/rate_limits_spec.rb42
-rw-r--r--qa/qa/specs/features/api/1_manage/users_spec.rb2
-rw-r--r--qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb47
-rw-r--r--qa/qa/specs/features/api/3_create/repository/files_spec.rb8
-rw-r--r--qa/qa/specs/features/api/4_verify/remove_runner_spec.rb36
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb51
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb9
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb98
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb13
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/user/user_access_termination_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/mentions_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb12
-rw-r--r--qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb4
-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_mirroring_lfs_over_http_spec.rb7
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb2
-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/snippet/create_personal_snippet_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb5
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb53
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb149
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb314
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb219
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb233
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb (renamed from qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb)2
-rw-r--r--qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb164
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb4
-rw-r--r--qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb2
-rw-r--r--qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb10
-rw-r--r--qa/qa/specs/runner.rb4
-rw-r--r--qa/qa/support/dates.rb4
-rw-r--r--qa/qa/support/formatters/allure_metadata_formatter.rb87
-rw-r--r--qa/qa/support/formatters/test_stats_formatter.rb86
-rw-r--r--qa/qa/support/influxdb_tools.rb83
-rw-r--r--qa/qa/support/loglinking.rb86
-rw-r--r--qa/qa/support/page_error_checker.rb18
-rw-r--r--qa/qa/support/repeater.rb5
-rw-r--r--qa/qa/tools/delete_test_resources.rb100
-rw-r--r--qa/qa/tools/delete_test_users.rb87
-rw-r--r--qa/qa/tools/reliable_report.rb30
-rw-r--r--qa/qa/tools/test_resource_data_processor.rb8
-rw-r--r--qa/qa/tools/test_resources_handler.rb180
-rw-r--r--qa/spec/page/logging_spec.rb19
-rw-r--r--qa/spec/resource/api_fabricator_spec.rb47
-rw-r--r--qa/spec/resource/reusable_collection_spec.rb22
-rw-r--r--qa/spec/runtime/env_spec.rb32
-rw-r--r--qa/spec/runtime/logger_spec.rb30
-rw-r--r--qa/spec/spec_helper.rb13
-rw-r--r--qa/spec/specs/allure_report_spec.rb5
-rw-r--r--qa/spec/support/formatters/allure_metadata_formatter_spec.rb3
-rw-r--r--qa/spec/support/formatters/test_stats_formatter_spec.rb20
-rw-r--r--qa/spec/support/loglinking_spec.rb185
-rw-r--r--qa/spec/support/page_error_checker_spec.rb66
-rw-r--r--qa/spec/support/repeater_spec.rb6
-rw-r--r--qa/spec/tools/reliable_report_spec.rb2
-rw-r--r--qa/spec/tools/test_resources_data_processor_spec.rb28
-rw-r--r--qa/tasks/contracts.rake47
147 files changed, 4098 insertions, 1075 deletions
diff --git a/qa/Gemfile b/qa/Gemfile
index 1eaf9c7cec0..05acab5653f 100644
--- a/qa/Gemfile
+++ b/qa/Gemfile
@@ -4,7 +4,7 @@ source 'https://rubygems.org'
gem 'gitlab-qa', require: 'gitlab/qa'
gem 'activesupport', '~> 6.1.4.6' # This should stay in sync with the root's Gemfile
-gem 'allure-rspec', '~> 2.15.0'
+gem 'allure-rspec', '~> 2.16.0'
gem 'capybara', '~> 3.35.0'
gem 'capybara-screenshot', '~> 1.0.23'
gem 'rake', '~> 13'
@@ -35,6 +35,8 @@ gem 'confiner', '~> 0.2'
gem 'chemlab', '~> 0.9'
gem 'chemlab-library-www-gitlab-com', '~> 0.1'
+gem "pact", "~> 1.12"
+
gem 'deprecation_toolkit', '~> 1.5.1', require: false
group :development do
diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock
index 6afd205a6e1..4be8adaef33 100644
--- a/qa/Gemfile.lock
+++ b/qa/Gemfile.lock
@@ -2,7 +2,7 @@ GEM
remote: https://rubygems.org/
specs:
abstract_type (0.0.7)
- activesupport (6.1.4.6)
+ activesupport (6.1.4.7)
concurrent-ruby (~> 1.0, >= 1.0.2)
i18n (>= 1.6, < 2)
minitest (>= 5.1)
@@ -19,15 +19,16 @@ GEM
rack-test (>= 1.1.0, < 2.0)
rest-client (>= 2.0.2, < 3.0)
rspec (~> 3.8)
- allure-rspec (2.15.0)
- allure-ruby-commons (= 2.15.0)
+ allure-rspec (2.16.1)
+ allure-ruby-commons (= 2.16.1)
rspec-core (>= 3.8, < 4)
- allure-ruby-commons (2.15.0)
+ allure-ruby-commons (2.16.1)
mime-types (>= 3.3, < 4)
oj (>= 3.10, < 4)
require_all (>= 2, < 4)
uuid (>= 2.3, < 3)
ast (2.4.2)
+ awesome_print (1.9.2)
binding_ninja (0.2.3)
builder (3.2.4)
byebug (9.1.0)
@@ -57,7 +58,7 @@ GEM
adamantium (~> 0.2.0)
equalizer (~> 0.0.9)
concurrent-ruby (1.1.9)
- confiner (0.2.1)
+ confiner (0.2.3)
gitlab (>= 4.17)
zeitwerk (~> 2.5.1)
declarative (0.0.20)
@@ -87,10 +88,12 @@ GEM
faraday-net_http (1.0.1)
faraday-net_http_persistent (1.2.0)
faraday-patron (1.0.0)
- ffi (1.15.4)
+ ffi (1.15.5)
ffi-compiler (1.0.1)
ffi (>= 1.0.0)
rake
+ filelock (1.1.1)
+ find_a_port (1.0.1)
fog-core (2.1.0)
builder
excon (~> 0.58)
@@ -118,11 +121,12 @@ GEM
gitlab (4.18.0)
httparty (~> 0.18)
terminal-table (>= 1.5.1)
- gitlab-qa (7.17.1)
+ gitlab-qa (7.24.4)
activesupport (~> 6.1)
gitlab (~> 4.18.0)
http (~> 5.0)
nokogiri (~> 1.10)
+ rainbow (~> 3.0.0)
table_print (= 1.5.7)
google-apis-compute_v1 (0.21.0)
google-apis-core (>= 0.4, < 2.a)
@@ -169,10 +173,11 @@ GEM
mime-types (~> 3.0)
multi_xml (>= 0.5.2)
httpclient (2.8.3)
- i18n (1.8.11)
+ i18n (1.10.0)
concurrent-ruby (~> 1.0)
ice_nine (0.11.2)
influxdb-client (1.17.0)
+ json (2.6.1)
jwt (2.3.0)
knapsack (4.0.0)
rake
@@ -191,20 +196,43 @@ GEM
mime-types-data (~> 3.2015)
mime-types-data (3.2022.0105)
mini_mime (1.1.0)
- mini_portile2 (2.6.1)
+ mini_portile2 (2.8.0)
minitest (5.15.0)
multi_json (1.15.0)
multi_xml (0.6.0)
multipart-post (2.1.1)
netrc (0.11.0)
- nokogiri (1.12.5)
- mini_portile2 (~> 2.6.1)
+ nokogiri (1.13.3)
+ mini_portile2 (~> 2.8.0)
racc (~> 1.4)
octokit (4.21.0)
faraday (>= 0.9)
sawyer (~> 0.8.0, >= 0.5.3)
- oj (3.13.8)
+ oj (3.13.11)
os (1.1.4)
+ pact (1.59.0)
+ pact-mock_service (~> 3.0, >= 3.3.1)
+ pact-support (~> 1.15)
+ rack-test (>= 0.6.3, < 2.0.0)
+ rspec (~> 3.0)
+ term-ansicolor (~> 1.0)
+ thor (>= 0.20, < 2.0)
+ webrick (~> 1.3)
+ pact-mock_service (3.6.2)
+ filelock (~> 1.1)
+ find_a_port (~> 1.0.1)
+ json
+ pact-support (~> 1.12, >= 1.12.0)
+ rack (~> 2.0)
+ rspec (>= 2.14)
+ term-ansicolor (~> 1.0)
+ thor (>= 0.19, < 2.0)
+ webrick (~> 1.3)
+ pact-support (1.15.1)
+ awesome_print (~> 1.1)
+ randexp (~> 0.1.7)
+ rspec (>= 2.14)
+ term-ansicolor (~> 1.0)
parallel (1.19.2)
parallel_tests (2.29.0)
parallel
@@ -228,6 +256,7 @@ GEM
rack (>= 1.0, < 3)
rainbow (3.0.0)
rake (13.0.6)
+ randexp (0.1.7)
regexp_parser (2.1.1)
representable (3.1.1)
declarative (< 0.1.0)
@@ -282,19 +311,25 @@ GEM
jwt (>= 1.5, < 3.0)
multi_json (~> 1.10)
slack-notifier (2.4.0)
+ sync (0.5.0)
systemu (2.6.5)
table_print (1.5.7)
+ term-ansicolor (1.7.1)
+ tins (~> 1.0)
terminal-table (3.0.2)
unicode-display_width (>= 1.1.1, < 3)
+ thor (1.2.1)
thread_safe (0.3.6)
timecop (0.9.1)
+ tins (1.31.0)
+ sync
trailblazer-option (0.1.2)
tzinfo (2.0.4)
concurrent-ruby (~> 1.0)
uber (0.1.0)
unf (0.1.4)
unf_ext
- unf_ext (0.0.8)
+ unf_ext (0.0.8.1)
unicode-display_width (2.1.0)
unparser (0.4.7)
abstract_type (~> 0.0.7)
@@ -316,7 +351,7 @@ GEM
webrick (1.7.0)
xpath (3.2.0)
nokogiri (~> 1.8)
- zeitwerk (2.5.3)
+ zeitwerk (2.5.4)
PLATFORMS
ruby
@@ -324,7 +359,7 @@ PLATFORMS
DEPENDENCIES
activesupport (~> 6.1.4.6)
airborne (~> 0.3.4)
- allure-rspec (~> 2.15.0)
+ allure-rspec (~> 2.16.0)
capybara (~> 3.35.0)
capybara-screenshot (~> 1.0.23)
chemlab (~> 0.9)
@@ -337,6 +372,7 @@ DEPENDENCIES
influxdb-client (~> 1.17)
knapsack (~> 4.0)
octokit (~> 4.21)
+ pact (~> 1.12)
parallel (~> 1.19)
parallel_tests (~> 2.29)
pry-byebug (~> 3.5.1)
diff --git a/qa/Rakefile b/qa/Rakefile
index 5d8c49a399b..0a65a58e6fc 100644
--- a/qa/Rakefile
+++ b/qa/Rakefile
@@ -60,8 +60,25 @@ task :delete_projects do
QA::Tools::DeleteProjects.new.run
end
-desc "Deletes resources created during E2E test runs"
-task :delete_test_resources, :file_pattern do |t, args|
- QA::Tools::DeleteTestResources.new(args[:file_pattern]).run
+desc "Deletes test users"
+task :delete_test_users, [:delete_before, :dry_run, :exclude_users] do |t, args|
+ QA::Tools::DeleteTestUsers.new(args).run
+end
+
+namespace :test_resources do
+ desc "Deletes resources created during E2E test runs"
+ task :delete, [:file_pattern] do |t, args|
+ QA::Tools::TestResourcesHandler.new(args[:file_pattern]).run_delete
+ end
+
+ desc "Upload test resources JSON files to GCS"
+ task :upload, [:file_pattern, :ci_project_name] do |t, args|
+ QA::Tools::TestResourcesHandler.new(args[:file_pattern]).upload(args[:ci_project_name])
+ end
+
+ desc "Download test resources JSON files from GCS"
+ task :download, [:ci_project_name] do |t, args|
+ QA::Tools::TestResourcesHandler.new.download(args[:ci_project_name])
+ end
end
# rubocop:enable Rails/RakeEnvironment
diff --git a/qa/contracts/.gitignore b/qa/contracts/.gitignore
new file mode 100644
index 00000000000..cb89d4102d3
--- /dev/null
+++ b/qa/contracts/.gitignore
@@ -0,0 +1,2 @@
+logs/
+consumer/node_modules
diff --git a/qa/contracts/consumer/.node-version b/qa/contracts/consumer/.node-version
new file mode 100644
index 00000000000..18711d290ea
--- /dev/null
+++ b/qa/contracts/consumer/.node-version
@@ -0,0 +1 @@
+14.17.5
diff --git a/qa/contracts/consumer/endpoints/merge_request.js b/qa/contracts/consumer/endpoints/merge_request.js
new file mode 100644
index 00000000000..74fd4e75bec
--- /dev/null
+++ b/qa/contracts/consumer/endpoints/merge_request.js
@@ -0,0 +1,42 @@
+'use strict';
+
+const axios = require('axios');
+
+exports.getMetadata = (endpoint) => {
+ const url = endpoint.url;
+
+ return axios
+ .request({
+ method: 'GET',
+ baseURL: url,
+ url: '/diffs_metadata.json',
+ headers: { Accept: '*/*' },
+ })
+ .then((response) => response.data);
+};
+
+exports.getDiscussions = (endpoint) => {
+ const url = endpoint.url;
+
+ return axios
+ .request({
+ method: 'GET',
+ baseURL: url,
+ url: '/discussions.json',
+ headers: { Accept: '*/*' },
+ })
+ .then((response) => response.data);
+};
+
+exports.getDiffs = (endpoint) => {
+ const url = endpoint.url;
+
+ return axios
+ .request({
+ method: 'GET',
+ baseURL: url,
+ url: '/diffs_batch.json?page=0',
+ headers: { Accept: '*/*' },
+ })
+ .then((response) => response.data);
+};
diff --git a/qa/contracts/consumer/fixtures/diffs.fixture.js b/qa/contracts/consumer/fixtures/diffs.fixture.js
new file mode 100644
index 00000000000..286d71f421c
--- /dev/null
+++ b/qa/contracts/consumer/fixtures/diffs.fixture.js
@@ -0,0 +1,89 @@
+'use strict';
+
+const { Matchers } = require('@pact-foundation/pact');
+
+const body = {
+ diff_files: Matchers.eachLike({
+ content_sha: Matchers.string('b0c94059db75b2473d616d4b1fde1a77533355a3'),
+ submodule: Matchers.boolean(false),
+ edit_path: Matchers.string('/gitlab-qa-bot/...'),
+ ide_edit_path: Matchers.string('/gitlab-qa-bot/...'),
+ old_path_html: Matchers.string('Gemfile'),
+ new_path_html: Matchers.string('Gemfile'),
+ blob: {
+ id: Matchers.string('855071bb3928d140764885964f7be1bb3e582495'),
+ path: Matchers.string('Gemfile'),
+ name: Matchers.string('Gemfile'),
+ mode: Matchers.string('1234567'),
+ readable_text: Matchers.boolean(true),
+ icon: Matchers.string('doc-text'),
+ },
+ can_modify_blob: Matchers.boolean(false),
+ file_identifier_hash: Matchers.string('67d82b8716a5b6c52c7abf0b2cd99c7594ed3587'),
+ file_hash: Matchers.string('67d82b8716a5b6c52c7abf0b2cd99c7594ed3587'),
+ file_path: Matchers.string('Gemfile'),
+ old_path: Matchers.string('Gemfile'),
+ new_path: Matchers.string('Gemfile'),
+ new_file: Matchers.boolean(false),
+ renamed_file: Matchers.boolean(false),
+ deleted_file: Matchers.boolean(false),
+ diff_refs: {
+ base_sha: Matchers.string('67d82b8716a5b6c52c7abf0b2cd99c7594ed3587'),
+ start_sha: Matchers.string('67d82b8716a5b6c52c7abf0b2cd99c7594ed3587'),
+ head_sha: Matchers.string('67d82b8716a5b6c52c7abf0b2cd99c7594ed3587'),
+ },
+ mode_changed: Matchers.boolean(false),
+ a_mode: Matchers.string('123456'),
+ b_mode: Matchers.string('123456'),
+ viewer: {
+ name: Matchers.string('text'),
+ collapsed: Matchers.boolean(false),
+ },
+ old_size: Matchers.integer(2288),
+ new_size: Matchers.integer(2288),
+ added_lines: Matchers.integer(1),
+ removed_lines: Matchers.integer(1),
+ load_collapsed_diff_url: Matchers.string('/gitlab-qa-bot/...'),
+ view_path: Matchers.string('/gitlab-qa-bot/...'),
+ context_lines_path: Matchers.string('/gitlab-qa-bot/...'),
+ highlighted_diff_lines: Matchers.eachLike({
+ // The following values can also be null which is not supported
+ //line_code: Matchers.string('de3150c01c3a946a6168173c4116741379fe3579_1_1'),
+ //old_line: Matchers.integer(1),
+ //new_line: Matchers.integer(1),
+ text: Matchers.string('source'),
+ rich_text: Matchers.string('<span></span>'),
+ can_receive_suggestion: Matchers.boolean(true),
+ }),
+ is_fully_expanded: Matchers.boolean(false),
+ }),
+ pagination: {
+ total_pages: Matchers.integer(1),
+ },
+};
+
+const Diffs = {
+ body: Matchers.extractPayload(body),
+
+ success: {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ body: body,
+ },
+
+ request: {
+ uponReceiving: 'a request for diff lines',
+ withRequest: {
+ method: 'GET',
+ path: '/diffs_batch.json',
+ headers: {
+ Accept: '*/*',
+ },
+ query: 'page=0',
+ },
+ },
+};
+
+exports.Diffs = Diffs;
diff --git a/qa/contracts/consumer/fixtures/discussions.fixture.js b/qa/contracts/consumer/fixtures/discussions.fixture.js
new file mode 100644
index 00000000000..cfc6112561b
--- /dev/null
+++ b/qa/contracts/consumer/fixtures/discussions.fixture.js
@@ -0,0 +1,85 @@
+'use strict';
+
+const { Matchers } = require('@pact-foundation/pact');
+
+const body = Matchers.eachLike({
+ id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
+ reply_id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
+ project_id: Matchers.integer(6954442),
+ confidential: Matchers.boolean(false),
+ diff_discussion: Matchers.boolean(false),
+ expanded: Matchers.boolean(false),
+ for_commit: Matchers.boolean(false),
+ individual_note: Matchers.boolean(true),
+ resolvable: Matchers.boolean(false),
+ resolved_by_push: Matchers.boolean(false),
+ notes: Matchers.eachLike({
+ id: Matchers.string('76489845'),
+ author: {
+ id: Matchers.integer(1675733),
+ username: Matchers.string('gitlab-qa-bot'),
+ name: Matchers.string('gitlab-qa-bot'),
+ state: Matchers.string('active'),
+ avatar_url: Matchers.string(
+ 'https://secure.gravatar.com/avatar/8355ad0f2761367fae6b9c4fe80994b9?s=80&d=identicon',
+ ),
+ show_status: Matchers.boolean(false),
+ path: Matchers.string('/gitlab-qa-bot'),
+ },
+ created_at: Matchers.iso8601DateTimeWithMillis('2022-02-22T07:06:55.038Z'),
+ updated_at: Matchers.iso8601DateTimeWithMillis('2022-02-22T07:06:55.038Z'),
+ system: Matchers.boolean(false),
+ noteable_id: Matchers.integer(8333422),
+ noteable_type: Matchers.string('MergeRequest'),
+ resolvable: Matchers.boolean(false),
+ resolved: Matchers.boolean(true),
+ confidential: Matchers.boolean(false),
+ noteable_iid: Matchers.integer(1),
+ note: Matchers.string('This is a test comment'),
+ note_html: Matchers.string(
+ '<p data-sourcepos="1:1-1:22" dir="auto">This is a test comment</p>',
+ ),
+ current_user: {
+ can_edit: Matchers.boolean(true),
+ can_award_emoji: Matchers.boolean(true),
+ can_resolve: Matchers.boolean(false),
+ can_resolve_discussion: Matchers.boolean(false),
+ },
+ is_noteable_author: Matchers.boolean(true),
+ discussion_id: Matchers.string('fd73763cbcbf7b29eb8765d969a38f7d735e222a'),
+ emoji_awardable: Matchers.boolean(true),
+ report_abuse_path: Matchers.string('/gitlab-qa-bot/...'),
+ noteable_note_url: Matchers.string('https://staging.gitlab.com/gitlab-qa-bot/...'),
+ cached_markdown_version: Matchers.integer(1900552),
+ human_access: Matchers.string('Maintainer'),
+ is_contributor: Matchers.boolean(false),
+ project_name: Matchers.string('contract-testing'),
+ path: Matchers.string('/gitlab-qa-bot/...'),
+ }),
+ resolved: Matchers.boolean(true),
+});
+
+const Discussions = {
+ body: Matchers.extractPayload(body),
+
+ success: {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ body: body,
+ },
+
+ request: {
+ uponReceiving: 'a request for discussions',
+ withRequest: {
+ method: 'GET',
+ path: '/discussions.json',
+ headers: {
+ Accept: '*/*',
+ },
+ },
+ },
+};
+
+exports.Discussions = Discussions;
diff --git a/qa/contracts/consumer/fixtures/metadata.fixture.js b/qa/contracts/consumer/fixtures/metadata.fixture.js
new file mode 100644
index 00000000000..05a4831c447
--- /dev/null
+++ b/qa/contracts/consumer/fixtures/metadata.fixture.js
@@ -0,0 +1,96 @@
+'use strict';
+
+const { Matchers } = require('@pact-foundation/pact');
+
+const body = {
+ real_size: Matchers.string('1'),
+ size: Matchers.integer(1),
+ branch_name: Matchers.string('testing-branch-1'),
+ source_branch_exists: Matchers.boolean(true),
+ target_branch_name: Matchers.string('master'),
+ merge_request_diff: {
+ created_at: Matchers.iso8601DateTimeWithMillis('2022-02-17T11:47:08.804Z'),
+ commits_count: Matchers.integer(1),
+ latest: Matchers.boolean(true),
+ short_commit_sha: Matchers.string('aee1ffec'),
+ base_version_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773',
+ ),
+ head_version_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_head=true',
+ ),
+ version_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773',
+ ),
+ compare_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773&start_sha=aee1ffec2299c0cfb17c8821e931339b73a3759f',
+ ),
+ },
+ latest_diff: Matchers.boolean(true),
+ latest_version_path: Matchers.string('/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs'),
+ added_lines: Matchers.integer(1),
+ removed_lines: Matchers.integer(1),
+ render_overflow_warning: Matchers.boolean(false),
+ email_patch_path: Matchers.string('/gitlab-qa-bot/contract-testing/-/merge_requests/1.patch'),
+ plain_diff_path: Matchers.string('/gitlab-qa-bot/contract-testing/-/merge_requests/1.diff'),
+ merge_request_diffs: Matchers.eachLike({
+ commits_count: Matchers.integer(1),
+ latest: Matchers.boolean(true),
+ short_commit_sha: Matchers.string('aee1ffec'),
+ base_version_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773',
+ ),
+ head_version_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_head=true',
+ ),
+ version_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773',
+ ),
+ compare_path: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773&start_sha=aee1ffec2299c0cfb17c8821e931339b73a3759f',
+ ),
+ }),
+ definition_path_prefix: Matchers.string(
+ '/gitlab-qa-bot/contract-testing/-/blob/aee1ffec2299c0cfb17c8821e931339b73a3759f',
+ ),
+ diff_files: Matchers.eachLike({
+ added_lines: Matchers.integer(1),
+ removed_lines: Matchers.integer(1),
+ new_path: Matchers.string('Gemfile'),
+ old_path: Matchers.string('Gemfile'),
+ new_file: Matchers.boolean(false),
+ deleted_file: Matchers.boolean(false),
+ submodule: Matchers.boolean(false),
+ file_identifier_hash: Matchers.string('67d82b8716a5b6c52c7abf0b2cd99c7594ed3587'),
+ file_hash: Matchers.string('de3150c01c3a946a6168173c4116741379fe3579'),
+ }),
+ has_conflicts: Matchers.boolean(false),
+ can_merge: Matchers.boolean(false),
+ project_path: Matchers.string('gitlab-qa-bot/contract-testing'),
+ project_name: Matchers.string('contract-testing'),
+};
+
+const Metadata = {
+ body: Matchers.extractPayload(body),
+
+ success: {
+ status: 200,
+ headers: {
+ 'Content-Type': 'application/json; charset=utf-8',
+ },
+ body: body,
+ },
+
+ request: {
+ uponReceiving: 'a request for Metadata',
+ withRequest: {
+ method: 'GET',
+ path: '/diffs_metadata.json',
+ headers: {
+ Accept: '*/*',
+ },
+ },
+ },
+};
+
+exports.Metadata = Metadata;
diff --git a/qa/contracts/consumer/package.json b/qa/contracts/consumer/package.json
new file mode 100644
index 00000000000..b4a3f59e89e
--- /dev/null
+++ b/qa/contracts/consumer/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "consumer",
+ "version": "1.0.0",
+ "description": "consumer side contract testing",
+ "license": "MIT",
+ "repository": "https://gitlab.com/gitlab-org/gitlab.git",
+ "dependencies": {
+ "@pact-foundation/pact": "^9.17.2",
+ "axios": "^0.26.0",
+ "jest": "^27.5.1",
+ "jest-pact": "^0.9.1",
+ "prettier": "^2.5.1"
+ },
+ "scripts": {
+ "test": "jest specs/ --runInBand"
+ }
+}
diff --git a/qa/contracts/consumer/specs/diffs.spec.js b/qa/contracts/consumer/specs/diffs.spec.js
new file mode 100644
index 00000000000..5be2ed7ac00
--- /dev/null
+++ b/qa/contracts/consumer/specs/diffs.spec.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const { pactWith } = require('jest-pact');
+
+const { Diffs } = require('../fixtures/diffs.fixture');
+const { getDiffs } = require('../endpoints/merge_request');
+
+pactWith(
+ {
+ consumer: 'Merge Request Page',
+ provider: 'Merge Request Diffs Endpoint',
+ log: '../logs/consumer.log',
+ dir: '../contracts',
+ },
+
+ (provider) => {
+ describe('Diffs Endpoint', () => {
+ beforeEach(() => {
+ const interaction = {
+ ...Diffs.request,
+ willRespondWith: Diffs.success,
+ };
+ return provider.addInteraction(interaction);
+ });
+
+ it('return a successful body', () => {
+ return getDiffs({
+ url: provider.mockService.baseUrl,
+ }).then((diffs) => {
+ expect(diffs).toEqual(Diffs.body);
+ });
+ });
+ });
+ },
+);
diff --git a/qa/contracts/consumer/specs/discussions.spec.js b/qa/contracts/consumer/specs/discussions.spec.js
new file mode 100644
index 00000000000..28ee3186a9f
--- /dev/null
+++ b/qa/contracts/consumer/specs/discussions.spec.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const { pactWith } = require('jest-pact');
+
+const { Discussions } = require('../fixtures/discussions.fixture');
+const { getDiscussions } = require('../endpoints/merge_request');
+
+pactWith(
+ {
+ consumer: 'Merge Request Page',
+ provider: 'Merge Request Discussions Endpoint',
+ log: '../logs/consumer.log',
+ dir: '../contracts',
+ },
+
+ (provider) => {
+ describe('Discussions Endpoint', () => {
+ beforeEach(() => {
+ const interaction = {
+ ...Discussions.request,
+ willRespondWith: Discussions.success,
+ };
+ return provider.addInteraction(interaction);
+ });
+
+ it('return a successful body', () => {
+ return getDiscussions({
+ url: provider.mockService.baseUrl,
+ }).then((discussions) => {
+ expect(discussions).toEqual(Discussions.body);
+ });
+ });
+ });
+ },
+);
diff --git a/qa/contracts/consumer/specs/metadata.spec.js b/qa/contracts/consumer/specs/metadata.spec.js
new file mode 100644
index 00000000000..31fc398f228
--- /dev/null
+++ b/qa/contracts/consumer/specs/metadata.spec.js
@@ -0,0 +1,35 @@
+'use strict';
+
+const { pactWith } = require('jest-pact');
+
+const { Metadata } = require('../fixtures/metadata.fixture');
+const { getMetadata } = require('../endpoints/merge_request');
+
+pactWith(
+ {
+ consumer: 'Merge Request Page',
+ provider: 'Merge Request Metadata Endpoint',
+ log: '../logs/consumer.log',
+ dir: '../contracts',
+ },
+
+ (provider) => {
+ describe('Metadata Endpoint', () => {
+ beforeEach(() => {
+ const interaction = {
+ ...Metadata.request,
+ willRespondWith: Metadata.success,
+ };
+ return provider.addInteraction(interaction);
+ });
+
+ it('return a successful body', () => {
+ return getMetadata({
+ url: provider.mockService.baseUrl,
+ }).then((metadata) => {
+ expect(metadata).toEqual(Metadata.body);
+ });
+ });
+ });
+ },
+);
diff --git a/qa/contracts/contracts/merge_request_page-merge_request_diffs_endpoint.json b/qa/contracts/contracts/merge_request_page-merge_request_diffs_endpoint.json
new file mode 100644
index 00000000000..8df54c25326
--- /dev/null
+++ b/qa/contracts/contracts/merge_request_page-merge_request_diffs_endpoint.json
@@ -0,0 +1,228 @@
+{
+ "consumer": {
+ "name": "Merge Request Page"
+ },
+ "provider": {
+ "name": "Merge Request Diffs Endpoint"
+ },
+ "interactions": [
+ {
+ "description": "a request for diff lines",
+ "request": {
+ "method": "GET",
+ "path": "/diffs_batch.json",
+ "query": "page=0",
+ "headers": {
+ "Accept": "*/*"
+ }
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8"
+ },
+ "body": {
+ "diff_files": [
+ {
+ "content_sha": "b0c94059db75b2473d616d4b1fde1a77533355a3",
+ "submodule": false,
+ "edit_path": "/gitlab-qa-bot/...",
+ "ide_edit_path": "/gitlab-qa-bot/...",
+ "old_path_html": "Gemfile",
+ "new_path_html": "Gemfile",
+ "blob": {
+ "id": "855071bb3928d140764885964f7be1bb3e582495",
+ "path": "Gemfile",
+ "name": "Gemfile",
+ "mode": "1234567",
+ "readable_text": true,
+ "icon": "doc-text"
+ },
+ "can_modify_blob": false,
+ "file_identifier_hash": "67d82b8716a5b6c52c7abf0b2cd99c7594ed3587",
+ "file_hash": "67d82b8716a5b6c52c7abf0b2cd99c7594ed3587",
+ "file_path": "Gemfile",
+ "old_path": "Gemfile",
+ "new_path": "Gemfile",
+ "new_file": false,
+ "renamed_file": false,
+ "deleted_file": false,
+ "diff_refs": {
+ "base_sha": "67d82b8716a5b6c52c7abf0b2cd99c7594ed3587",
+ "start_sha": "67d82b8716a5b6c52c7abf0b2cd99c7594ed3587",
+ "head_sha": "67d82b8716a5b6c52c7abf0b2cd99c7594ed3587"
+ },
+ "mode_changed": false,
+ "a_mode": "123456",
+ "b_mode": "123456",
+ "viewer": {
+ "name": "text",
+ "collapsed": false
+ },
+ "old_size": 2288,
+ "new_size": 2288,
+ "added_lines": 1,
+ "removed_lines": 1,
+ "load_collapsed_diff_url": "/gitlab-qa-bot/...",
+ "view_path": "/gitlab-qa-bot/...",
+ "context_lines_path": "/gitlab-qa-bot/...",
+ "highlighted_diff_lines": [
+ {
+ "text": "source",
+ "rich_text": "<span></span>",
+ "can_receive_suggestion": true
+ }
+ ],
+ "is_fully_expanded": false
+ }
+ ],
+ "pagination": {
+ "total_pages": 1
+ }
+ },
+ "matchingRules": {
+ "$.body.diff_files": {
+ "min": 1
+ },
+ "$.body.diff_files[*].*": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].content_sha": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].submodule": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].edit_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].ide_edit_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].old_path_html": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].new_path_html": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].blob.id": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].blob.path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].blob.name": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].blob.mode": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].blob.readable_text": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].blob.icon": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].can_modify_blob": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].file_identifier_hash": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].file_hash": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].file_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].old_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].new_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].new_file": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].renamed_file": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].deleted_file": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].diff_refs.base_sha": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].diff_refs.start_sha": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].diff_refs.head_sha": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].mode_changed": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].a_mode": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].b_mode": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].viewer.name": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].viewer.collapsed": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].old_size": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].new_size": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].added_lines": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].removed_lines": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].load_collapsed_diff_url": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].view_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].context_lines_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].highlighted_diff_lines": {
+ "min": 1
+ },
+ "$.body.diff_files[*].highlighted_diff_lines[*].*": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].highlighted_diff_lines[*].text": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].highlighted_diff_lines[*].rich_text": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].highlighted_diff_lines[*].can_receive_suggestion": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].is_fully_expanded": {
+ "match": "type"
+ },
+ "$.body.pagination.total_pages": {
+ "match": "type"
+ }
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "pactSpecification": {
+ "version": "2.0.0"
+ }
+ }
+} \ No newline at end of file
diff --git a/qa/contracts/contracts/merge_request_page-merge_request_discussions_endpoint.json b/qa/contracts/contracts/merge_request_page-merge_request_discussions_endpoint.json
new file mode 100644
index 00000000000..14839053e57
--- /dev/null
+++ b/qa/contracts/contracts/merge_request_page-merge_request_discussions_endpoint.json
@@ -0,0 +1,235 @@
+{
+ "consumer": {
+ "name": "Merge Request Page"
+ },
+ "provider": {
+ "name": "Merge Request Discussions Endpoint"
+ },
+ "interactions": [
+ {
+ "description": "a request for discussions",
+ "request": {
+ "method": "GET",
+ "path": "/discussions.json",
+ "headers": {
+ "Accept": "*/*"
+ }
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8"
+ },
+ "body": [
+ {
+ "id": "fd73763cbcbf7b29eb8765d969a38f7d735e222a",
+ "reply_id": "fd73763cbcbf7b29eb8765d969a38f7d735e222a",
+ "project_id": 6954442,
+ "confidential": false,
+ "diff_discussion": false,
+ "expanded": false,
+ "for_commit": false,
+ "individual_note": true,
+ "resolvable": false,
+ "resolved_by_push": false,
+ "notes": [
+ {
+ "id": "76489845",
+ "author": {
+ "id": 1675733,
+ "username": "gitlab-qa-bot",
+ "name": "gitlab-qa-bot",
+ "state": "active",
+ "avatar_url": "https://secure.gravatar.com/avatar/8355ad0f2761367fae6b9c4fe80994b9?s=80&d=identicon",
+ "show_status": false,
+ "path": "/gitlab-qa-bot"
+ },
+ "created_at": "2022-02-22T07:06:55.038Z",
+ "updated_at": "2022-02-22T07:06:55.038Z",
+ "system": false,
+ "noteable_id": 8333422,
+ "noteable_type": "MergeRequest",
+ "resolvable": false,
+ "resolved": true,
+ "confidential": false,
+ "noteable_iid": 1,
+ "note": "This is a test comment",
+ "note_html": "<p data-sourcepos=\"1:1-1:22\" dir=\"auto\">This is a test comment</p>",
+ "current_user": {
+ "can_edit": true,
+ "can_award_emoji": true,
+ "can_resolve": false,
+ "can_resolve_discussion": false
+ },
+ "is_noteable_author": true,
+ "discussion_id": "fd73763cbcbf7b29eb8765d969a38f7d735e222a",
+ "emoji_awardable": true,
+ "report_abuse_path": "/gitlab-qa-bot/...",
+ "noteable_note_url": "https://staging.gitlab.com/gitlab-qa-bot/...",
+ "cached_markdown_version": 1900552,
+ "human_access": "Maintainer",
+ "is_contributor": false,
+ "project_name": "contract-testing",
+ "path": "/gitlab-qa-bot/..."
+ }
+ ],
+ "resolved": true
+ }
+ ],
+ "matchingRules": {
+ "$.body": {
+ "min": 1
+ },
+ "$.body[*].*": {
+ "match": "type"
+ },
+ "$.body[*].id": {
+ "match": "type"
+ },
+ "$.body[*].reply_id": {
+ "match": "type"
+ },
+ "$.body[*].project_id": {
+ "match": "type"
+ },
+ "$.body[*].confidential": {
+ "match": "type"
+ },
+ "$.body[*].diff_discussion": {
+ "match": "type"
+ },
+ "$.body[*].expanded": {
+ "match": "type"
+ },
+ "$.body[*].for_commit": {
+ "match": "type"
+ },
+ "$.body[*].individual_note": {
+ "match": "type"
+ },
+ "$.body[*].resolvable": {
+ "match": "type"
+ },
+ "$.body[*].resolved_by_push": {
+ "match": "type"
+ },
+ "$.body[*].notes": {
+ "min": 1
+ },
+ "$.body[*].notes[*].*": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].id": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].author.id": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].author.username": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].author.name": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].author.state": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].author.avatar_url": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].author.show_status": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].author.path": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].created_at": {
+ "match": "regex",
+ "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
+ },
+ "$.body[*].notes[*].updated_at": {
+ "match": "regex",
+ "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
+ },
+ "$.body[*].notes[*].system": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].noteable_id": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].noteable_type": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].resolvable": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].resolved": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].confidential": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].noteable_iid": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].note": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].note_html": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].current_user.can_edit": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].current_user.can_award_emoji": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].current_user.can_resolve": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].current_user.can_resolve_discussion": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].is_noteable_author": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].discussion_id": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].emoji_awardable": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].report_abuse_path": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].noteable_note_url": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].cached_markdown_version": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].human_access": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].is_contributor": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].project_name": {
+ "match": "type"
+ },
+ "$.body[*].notes[*].path": {
+ "match": "type"
+ },
+ "$.body[*].resolved": {
+ "match": "type"
+ }
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "pactSpecification": {
+ "version": "2.0.0"
+ }
+ }
+} \ No newline at end of file
diff --git a/qa/contracts/contracts/merge_request_page-merge_request_metadata_endpoint.json b/qa/contracts/contracts/merge_request_page-merge_request_metadata_endpoint.json
new file mode 100644
index 00000000000..4b6cab0fc94
--- /dev/null
+++ b/qa/contracts/contracts/merge_request_page-merge_request_metadata_endpoint.json
@@ -0,0 +1,222 @@
+{
+ "consumer": {
+ "name": "Merge Request Page"
+ },
+ "provider": {
+ "name": "Merge Request Metadata Endpoint"
+ },
+ "interactions": [
+ {
+ "description": "a request for Metadata",
+ "request": {
+ "method": "GET",
+ "path": "/diffs_metadata.json",
+ "headers": {
+ "Accept": "*/*"
+ }
+ },
+ "response": {
+ "status": 200,
+ "headers": {
+ "Content-Type": "application/json; charset=utf-8"
+ },
+ "body": {
+ "real_size": "1",
+ "size": 1,
+ "branch_name": "testing-branch-1",
+ "source_branch_exists": true,
+ "target_branch_name": "master",
+ "merge_request_diff": {
+ "created_at": "2022-02-17T11:47:08.804Z",
+ "commits_count": 1,
+ "latest": true,
+ "short_commit_sha": "aee1ffec",
+ "base_version_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773",
+ "head_version_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_head=true",
+ "version_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773",
+ "compare_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773&start_sha=aee1ffec2299c0cfb17c8821e931339b73a3759f"
+ },
+ "latest_diff": true,
+ "latest_version_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs",
+ "added_lines": 1,
+ "removed_lines": 1,
+ "render_overflow_warning": false,
+ "email_patch_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1.patch",
+ "plain_diff_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1.diff",
+ "merge_request_diffs": [
+ {
+ "commits_count": 1,
+ "latest": true,
+ "short_commit_sha": "aee1ffec",
+ "base_version_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773",
+ "head_version_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_head=true",
+ "version_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773",
+ "compare_path": "/gitlab-qa-bot/contract-testing/-/merge_requests/1/diffs?diff_id=10581773&start_sha=aee1ffec2299c0cfb17c8821e931339b73a3759f"
+ }
+ ],
+ "definition_path_prefix": "/gitlab-qa-bot/contract-testing/-/blob/aee1ffec2299c0cfb17c8821e931339b73a3759f",
+ "diff_files": [
+ {
+ "added_lines": 1,
+ "removed_lines": 1,
+ "new_path": "Gemfile",
+ "old_path": "Gemfile",
+ "new_file": false,
+ "deleted_file": false,
+ "submodule": false,
+ "file_identifier_hash": "67d82b8716a5b6c52c7abf0b2cd99c7594ed3587",
+ "file_hash": "de3150c01c3a946a6168173c4116741379fe3579"
+ }
+ ],
+ "has_conflicts": false,
+ "can_merge": false,
+ "project_path": "gitlab-qa-bot/contract-testing",
+ "project_name": "contract-testing"
+ },
+ "matchingRules": {
+ "$.body.real_size": {
+ "match": "type"
+ },
+ "$.body.size": {
+ "match": "type"
+ },
+ "$.body.branch_name": {
+ "match": "type"
+ },
+ "$.body.source_branch_exists": {
+ "match": "type"
+ },
+ "$.body.target_branch_name": {
+ "match": "type"
+ },
+ "$.body.merge_request_diff.created_at": {
+ "match": "regex",
+ "regex": "^\\d{4}-[01]\\d-[0-3]\\dT[0-2]\\d:[0-5]\\d:[0-5]\\d\\.\\d+([+-][0-2]\\d(:?[0-5]\\d)?|Z)$"
+ },
+ "$.body.merge_request_diff.commits_count": {
+ "match": "type"
+ },
+ "$.body.merge_request_diff.latest": {
+ "match": "type"
+ },
+ "$.body.merge_request_diff.short_commit_sha": {
+ "match": "type"
+ },
+ "$.body.merge_request_diff.base_version_path": {
+ "match": "type"
+ },
+ "$.body.merge_request_diff.head_version_path": {
+ "match": "type"
+ },
+ "$.body.merge_request_diff.version_path": {
+ "match": "type"
+ },
+ "$.body.merge_request_diff.compare_path": {
+ "match": "type"
+ },
+ "$.body.latest_diff": {
+ "match": "type"
+ },
+ "$.body.latest_version_path": {
+ "match": "type"
+ },
+ "$.body.added_lines": {
+ "match": "type"
+ },
+ "$.body.removed_lines": {
+ "match": "type"
+ },
+ "$.body.render_overflow_warning": {
+ "match": "type"
+ },
+ "$.body.email_patch_path": {
+ "match": "type"
+ },
+ "$.body.plain_diff_path": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs": {
+ "min": 1
+ },
+ "$.body.merge_request_diffs[*].*": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs[*].commits_count": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs[*].latest": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs[*].short_commit_sha": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs[*].base_version_path": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs[*].head_version_path": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs[*].version_path": {
+ "match": "type"
+ },
+ "$.body.merge_request_diffs[*].compare_path": {
+ "match": "type"
+ },
+ "$.body.definition_path_prefix": {
+ "match": "type"
+ },
+ "$.body.diff_files": {
+ "min": 1
+ },
+ "$.body.diff_files[*].*": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].added_lines": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].removed_lines": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].new_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].old_path": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].new_file": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].deleted_file": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].submodule": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].file_identifier_hash": {
+ "match": "type"
+ },
+ "$.body.diff_files[*].file_hash": {
+ "match": "type"
+ },
+ "$.body.has_conflicts": {
+ "match": "type"
+ },
+ "$.body.can_merge": {
+ "match": "type"
+ },
+ "$.body.project_path": {
+ "match": "type"
+ },
+ "$.body.project_name": {
+ "match": "type"
+ }
+ }
+ }
+ }
+ ],
+ "metadata": {
+ "pactSpecification": {
+ "version": "2.0.0"
+ }
+ }
+} \ No newline at end of file
diff --git a/qa/contracts/provider/environments/base.rb b/qa/contracts/provider/environments/base.rb
new file mode 100644
index 00000000000..695ee6b867d
--- /dev/null
+++ b/qa/contracts/provider/environments/base.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module Provider
+ module Environments
+ class Base
+ attr_writer :base_url, :merge_request
+
+ def call(env)
+ @payload
+ end
+
+ def http(endpoint)
+ Faraday.default_adapter = :net_http
+ response = Faraday.get(@base_url + endpoint)
+ @payload = [response.status, response.headers, [response.body]]
+ self
+ end
+
+ def merge_request(endpoint)
+ http(@merge_request + endpoint) if endpoint.include? '.json'
+ end
+ end
+ end
+end
diff --git a/qa/contracts/provider/environments/local.rb b/qa/contracts/provider/environments/local.rb
new file mode 100644
index 00000000000..0d472bc25e9
--- /dev/null
+++ b/qa/contracts/provider/environments/local.rb
@@ -0,0 +1,12 @@
+# frozen_string_literal: true
+
+module Provider
+ module Environments
+ class Local < Base
+ def initialize
+ @base_url = ENV['CONTRACT_HOST']
+ @merge_request = ENV['CONTRACT_MR']
+ end
+ end
+ end
+end
diff --git a/qa/contracts/provider/spec/diffs_helper.rb b/qa/contracts/provider/spec/diffs_helper.rb
new file mode 100644
index 00000000000..95dbc4254e6
--- /dev/null
+++ b/qa/contracts/provider/spec/diffs_helper.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative '../spec_helper'
+
+module Provider
+ module DiffsHelper
+ local = Environments::Local.new
+
+ Pact.service_provider "Merge Request Diffs Endpoint" do
+ app { local.merge_request('/diffs_batch.json?page=0') }
+
+ honours_pact_with 'Merge Request Page' do
+ pact_uri '../contracts/merge_request_page-merge_request_diffs_endpoint.json'
+ end
+ end
+ end
+end
diff --git a/qa/contracts/provider/spec/discussions_helper.rb b/qa/contracts/provider/spec/discussions_helper.rb
new file mode 100644
index 00000000000..642dde79e1d
--- /dev/null
+++ b/qa/contracts/provider/spec/discussions_helper.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative '../spec_helper'
+
+module Provider
+ module DiscussionsHelper
+ local = Environments::Local.new
+
+ Pact.service_provider "Merge Request Discussions Endpoint" do
+ app { local.merge_request('/discussions.json') }
+
+ honours_pact_with 'Merge Request Page' do
+ pact_uri '../contracts/merge_request_page-merge_request_discussions_endpoint.json'
+ end
+ end
+ end
+end
diff --git a/qa/contracts/provider/spec/metadata_helper.rb b/qa/contracts/provider/spec/metadata_helper.rb
new file mode 100644
index 00000000000..a3eb4978641
--- /dev/null
+++ b/qa/contracts/provider/spec/metadata_helper.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+require_relative '../spec_helper'
+
+module Provider
+ module MetadataHelper
+ local = Environments::Local.new
+
+ Pact.service_provider "Merge Request Metadata Endpoint" do
+ app { local.merge_request('/diffs_metadata.json') }
+
+ honours_pact_with 'Merge Request Page' do
+ pact_uri '../contracts/merge_request_page-merge_request_metadata_endpoint.json'
+ end
+ end
+ end
+end
diff --git a/qa/contracts/provider/spec_helper.rb b/qa/contracts/provider/spec_helper.rb
new file mode 100644
index 00000000000..1869c039910
--- /dev/null
+++ b/qa/contracts/provider/spec_helper.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module SpecHelper
+ unless ENV['CONTRACT_HOST']
+ raise(ArgumentError, 'Contract tests require CONTRACT_HOST environment variable to be set!')
+ end
+
+ require_relative '../../../config/bundler_setup'
+ Bundler.require(:default)
+
+ root = File.expand_path('../', __dir__)
+
+ loader = Zeitwerk::Loader.new
+ loader.push_dir(root)
+
+ loader.ignore("#{root}/consumer")
+ loader.ignore("#{root}/contracts")
+
+ loader.collapse("#{root}/provider/spec")
+
+ loader.setup
+end
diff --git a/qa/knapsack/master_report.json b/qa/knapsack/master_report.json
index 0e9f67fa967..3880346a1b9 100644
--- a/qa/knapsack/master_report.json
+++ b/qa/knapsack/master_report.json
@@ -32,7 +32,6 @@
"qa/specs/features/ee/browser_ui/10_protect/policy_alerts_list_spec.rb": 14.055217947000074,
"qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb": 14.212461153999811,
"qa/specs/features/ee/api/2_plan/epics_milestone_dates_spec.rb": 14.218627059000028,
- "qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb": 14.295524570999987,
"qa/specs/features/api/1_manage/project_access_token_spec.rb": 14.394589879999785,
"qa/specs/features/ee/browser_ui/2_plan/multiple_assignees_for_issues/four_assignees_spec.rb": 14.505683429000328,
"qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb": 14.804579386000114,
diff --git a/qa/qa.rb b/qa/qa.rb
index 442f6c578cf..73bcb6de637 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -11,6 +11,12 @@ require_relative 'lib/gitlab'
require_relative '../config/bundler_setup'
Bundler.require(:default)
+require 'securerandom'
+require 'pathname'
+require 'active_support/core_ext/hash'
+require 'active_support/core_ext/object/blank'
+require 'rainbow/refinement'
+
module QA
root = "#{__dir__}/qa"
@@ -53,7 +59,8 @@ module QA
"jira_api" => "JiraAPI",
"registry_tls" => "RegistryTLS",
"jetbrains" => "JetBrains",
- "vscode" => "VSCode"
+ "vscode" => "VSCode",
+ "registry_with_cdn" => "RegistryWithCDN"
)
loader.setup
diff --git a/qa/qa/git/location.rb b/qa/qa/git/location.rb
index 032c6837db1..c3733572e70 100644
--- a/qa/qa/git/location.rb
+++ b/qa/qa/git/location.rb
@@ -9,6 +9,7 @@ module QA
extend Forwardable
attr_reader :git_uri, :uri
+
def_delegators :@uri, :user, :host, :path
# See: config/initializers/1_settings.rb
diff --git a/qa/qa/page/component/new_snippet.rb b/qa/qa/page/component/new_snippet.rb
index 673bc7ba44c..6ccf8a4043e 100644
--- a/qa/qa/page/component/new_snippet.rb
+++ b/qa/qa/page/component/new_snippet.rb
@@ -77,7 +77,10 @@ module QA
def click_create_snippet_button
wait_until(reload: false) { !find_element(:submit_button).disabled? }
- click_element(:submit_button, Page::Dashboard::Snippet::Show)
+ click_element(:submit_button)
+ wait_until(reload: false) do
+ has_no_element?(:snippet_title_field)
+ end
end
private
diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb
index 34e884f2a08..a8ae706858e 100644
--- a/qa/qa/page/component/snippet.rb
+++ b/qa/qa/page/component/snippet.rb
@@ -10,7 +10,7 @@ module QA
super
base.view 'app/assets/javascripts/snippets/components/snippet_title.vue' do
- element :snippet_title_content, required: true
+ element :snippet_title_content
end
base.view 'app/assets/javascripts/snippets/components/snippet_description_view.vue' do
@@ -87,7 +87,7 @@ module QA
end
def has_snippet_title?(snippet_title)
- has_element? :snippet_title_content, text: snippet_title
+ has_element?(:snippet_title_content, text: snippet_title, wait: 10)
end
def has_snippet_description?(snippet_description)
diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb
index c0108d85365..a0b42598962 100644
--- a/qa/qa/page/dashboard/projects.rb
+++ b/qa/qa/page/dashboard/projects.rb
@@ -23,6 +23,12 @@ module QA
end
end
+ def filter_by_name(name)
+ within_element(:project_filter_form) do
+ fill_in :name, with: name
+ end
+ end
+
def go_to_project(name)
filter_by_name(name)
@@ -40,14 +46,6 @@ module QA
def clear_project_filter
fill_element(:project_filter_form, "")
end
-
- private
-
- def filter_by_name(name)
- within_element(:project_filter_form) do
- fill_in :name, with: name
- end
- end
end
end
end
diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb
index 939413f6d76..d84a053591c 100644
--- a/qa/qa/page/dashboard/snippet/edit.rb
+++ b/qa/qa/page/dashboard/snippet/edit.rb
@@ -64,7 +64,10 @@ module QA
def save_changes
wait_until(reload: false) { !find_element(:submit_button).disabled? }
- click_element(:submit_button, Page::Dashboard::Snippet::Show)
+ click_element(:submit_button)
+ wait_until(reload: false) do
+ has_no_element?(:file_name_field)
+ end
end
private
diff --git a/qa/qa/page/dashboard/snippet/show.rb b/qa/qa/page/dashboard/snippet/show.rb
index a314f523108..f395bd4f8cb 100644
--- a/qa/qa/page/dashboard/snippet/show.rb
+++ b/qa/qa/page/dashboard/snippet/show.rb
@@ -9,7 +9,7 @@ module QA
include Page::Component::BlobContent
view 'app/assets/javascripts/snippets/components/snippet_title.vue' do
- element :snippet_title_content, required: true
+ element :snippet_title_content
end
end
end
diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb
index e54c3e0cd07..7d6d81cf869 100644
--- a/qa/qa/page/file/show.rb
+++ b/qa/qa/page/file/show.rb
@@ -33,15 +33,10 @@ module QA
end
def click_edit
- # TODO: remove this condition and else part once ff :consolidated_edit_button is enabled by default
- if has_element?(:action_dropdown)
- within_element(:action_dropdown) do
- click_button(class: 'dropdown-toggle-split')
- click_element(:edit_menu_item)
- click_element(:edit_button)
- end
- else
- click_on 'Edit'
+ within_element(:action_dropdown) do
+ click_button(class: 'dropdown-toggle-split')
+ click_element(:edit_menu_item)
+ click_element(:edit_button)
end
end
diff --git a/qa/qa/page/group/settings/general.rb b/qa/qa/page/group/settings/general.rb
index 1877065f478..86585eee121 100644
--- a/qa/qa/page/group/settings/general.rb
+++ b/qa/qa/page/group/settings/general.rb
@@ -102,16 +102,6 @@ module QA
click_element(:save_permissions_changes_button)
end
-
- def transfer_group(target_group, source_group)
- expand_content :advanced_settings_content
-
- select_namespace(target_group)
- click_element(:transfer_button)
-
- fill_confirmation_text(source_group)
- confirm_transfer
- end
end
end
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index a5bd37be287..c34b8f33a5d 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -4,6 +4,8 @@ module QA
module Page
module Main
class Login < Page::Base
+ include Layout::Flash
+
view 'app/views/devise/passwords/edit.html.haml' do
element :password_field
element :password_confirmation_field
@@ -176,6 +178,9 @@ module QA
Support::WaitForRequests.wait_for_requests
+ # For debugging invalid login attempts
+ has_notice?('Invalid login or password')
+
Page::Main::Terms.perform do |terms|
terms.accept_terms if terms.visible?
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
index d76dfb295a0..689b3dba286 100644
--- a/qa/qa/page/merge_request/show.rb
+++ b/qa/qa/page/merge_request/show.rb
@@ -116,7 +116,7 @@ module QA
end
view 'app/views/projects/merge_requests/_mr_box.html.haml' do
- element :title_content
+ element :title_content, required: true
end
view 'app/views/projects/merge_requests/_mr_title.html.haml' do
@@ -124,9 +124,9 @@ module QA
end
view 'app/views/projects/merge_requests/show.html.haml' do
- element :notes_tab
- element :commits_tab
- element :diffs_tab
+ element :notes_tab, required: true
+ element :commits_tab, required: true
+ element :diffs_tab, required: true
end
view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled.vue' do
diff --git a/qa/qa/page/modal/delete_issue.rb b/qa/qa/page/modal/delete_issue.rb
new file mode 100644
index 00000000000..9b51e969b48
--- /dev/null
+++ b/qa/qa/page/modal/delete_issue.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Modal
+ class DeleteIssue < Base
+ view 'app/assets/javascripts/issues/show/components/delete_issue_modal.vue' do
+ element :confirm_delete_issue_button, required: true
+ end
+
+ def confirm_delete_issue
+ click_element :confirm_delete_issue_button
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/fork/new.rb b/qa/qa/page/project/fork/new.rb
index cd743b648d8..e1b5e47dd0b 100644
--- a/qa/qa/page/project/fork/new.rb
+++ b/qa/qa/page/project/fork/new.rb
@@ -5,10 +5,6 @@ module QA
module Project
module Fork
class New < Page::Base
- view 'app/views/projects/forks/_fork_button.html.haml' do
- element :fork_namespace_button
- end
-
view 'app/assets/javascripts/pages/projects/forks/new/components/fork_form.vue' do
element :fork_namespace_dropdown
element :fork_project_button
@@ -16,13 +12,9 @@ module QA
end
def fork_project(namespace = Runtime::Namespace.path)
- if has_element?(:fork_namespace_button, wait: 0)
- click_element(:fork_namespace_button, name: namespace)
- else
- select_element(:fork_namespace_dropdown, namespace)
- click_element(:fork_privacy_button, privacy_level: 'public')
- click_element(:fork_project_button)
- end
+ select_element(:fork_namespace_dropdown, namespace)
+ click_element(:fork_privacy_button, privacy_level: 'public')
+ click_element(:fork_project_button)
end
def fork_namespace_dropdown_values
diff --git a/qa/qa/page/project/infrastructure/kubernetes/add.rb b/qa/qa/page/project/infrastructure/kubernetes/add.rb
deleted file mode 100644
index ed9ecb51a46..00000000000
--- a/qa/qa/page/project/infrastructure/kubernetes/add.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- module Page
- module Project
- module Infrastructure
- module Kubernetes
- class Add < Page::Base
- view 'app/views/clusters/clusters/new.html.haml' do
- element :add_existing_cluster_tab
- end
-
- def add_existing_cluster
- page.find('.gl-tab-nav-item', text: 'Connect existing cluster').click
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb
index b37210f4d3f..fe468de60cd 100644
--- a/qa/qa/page/project/issue/show.rb
+++ b/qa/qa/page/project/issue/show.rb
@@ -18,6 +18,8 @@ module QA
view 'app/assets/javascripts/issues/show/components/header_actions.vue' do
element :close_issue_button
element :reopen_issue_button
+ element :issue_actions_ellipsis_dropdown
+ element :delete_issue_button
end
view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do
@@ -69,6 +71,20 @@ module QA
def has_reopen_issue_button?
has_element?(:reopen_issue_button)
end
+
+ def has_delete_issue_button?
+ click_element(:issue_actions_ellipsis_dropdown)
+ has_element?(:delete_issue_button)
+ end
+
+ def delete_issue
+ click_element(:issue_actions_ellipsis_dropdown)
+ click_element(:delete_issue_button, Page::Modal::DeleteIssue)
+
+ Page::Modal::DeleteIssue.perform(&:confirm_delete_issue)
+
+ wait_for_requests
+ end
end
end
end
diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb
index e061bc52abc..340e40127c9 100644
--- a/qa/qa/page/project/new.rb
+++ b/qa/qa/page/project/new.rb
@@ -13,11 +13,11 @@ module QA
view 'app/views/projects/_new_project_fields.html.haml' do
element :initialize_with_readme_checkbox
- element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern
- element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern
- element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern
- element :project_create_button, "submit _('Create project')" # rubocop:disable QA/ElementWithPattern
- element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern
+ element :project_name
+ element :project_path
+ element :project_description
+ element :project_create_button
+ element :visibility_radios
end
view 'app/views/projects/_new_project_initialize_with_sast.html.haml' do
diff --git a/qa/qa/page/project/pipeline_editor/new.rb b/qa/qa/page/project/pipeline_editor/new.rb
new file mode 100644
index 00000000000..5d79dd86f2a
--- /dev/null
+++ b/qa/qa/page/project/pipeline_editor/new.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module QA
+ module Page
+ module Project
+ module PipelineEditor
+ class New < QA::Page::Base
+ view 'app/assets/javascripts/pipeline_editor/components/ui/pipeline_editor_empty_state.vue' do
+ element :create_new_ci_button, required: true
+ end
+
+ def create_new_ci
+ click_element(:create_new_ci_button, Page::Project::PipelineEditor::Show)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/pipeline_editor/show.rb b/qa/qa/page/project/pipeline_editor/show.rb
index 8289039d4c5..caf54a10025 100644
--- a/qa/qa/page/project/pipeline_editor/show.rb
+++ b/qa/qa/page/project/pipeline_editor/show.rb
@@ -6,13 +6,13 @@ module QA
module PipelineEditor
class Show < QA::Page::Base
view 'app/assets/javascripts/pipeline_editor/components/file_nav/branch_switcher.vue' do
- element :branch_selector_button, require: true
+ element :branch_selector_button, required: true
element :branch_menu_item_button
element :branch_menu_container
end
view 'app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue' do
- element :target_branch_field, require: true
+ element :target_branch_field, required: true
end
view 'app/assets/javascripts/pipeline_editor/components/drawer/pipeline_editor_drawer.vue' do
@@ -21,7 +21,7 @@ module QA
end
view 'app/assets/javascripts/vue_shared/components/source_editor.vue' do
- element :source_editor_container, require: true
+ element :source_editor_container, required: true
end
view 'app/assets/javascripts/pipeline_editor/components/header/pipeline_status.vue' do
@@ -30,6 +30,7 @@ module QA
view 'app/assets/javascripts/pipeline_editor/components/commit/commit_form.vue' do
element :commit_changes_button
+ element :new_mr_checkbox
end
view 'app/assets/javascripts/pipeline_editor/components/header/validation_segment.vue' do
@@ -76,6 +77,7 @@ module QA
end
def submit_changes
+ Support::Waiter.wait_until { !find_element(:commit_changes_button).disabled? }
click_element(:commit_changes_button)
wait_for_requests
@@ -127,6 +129,18 @@ module QA
end
end
+ def has_new_mr_checkbox?
+ has_element?(:new_mr_checkbox, visible: true)
+ end
+
+ def has_no_new_mr_checkbox?
+ has_no_element?(:new_mr_checkbox, visible: true)
+ end
+
+ def select_new_mr_checkbox
+ check_element(:new_mr_checkbox, true)
+ end
+
private
def go_to_tab(name)
diff --git a/qa/qa/page/project/settings/mirroring_repositories.rb b/qa/qa/page/project/settings/mirroring_repositories.rb
index 582079157f2..501b31f8a95 100644
--- a/qa/qa/page/project/settings/mirroring_repositories.rb
+++ b/qa/qa/page/project/settings/mirroring_repositories.rb
@@ -87,20 +87,21 @@ module QA
end
def update(url)
- row_index = find_repository_row_index url
+ row_index = find_repository_row_index(url)
within_element_by_index(:mirrored_repository_row, row_index) do
# When a repository is first mirrored, the update process might
# already be started, so the button is already "clicked"
click_element :update_now_button unless has_element? :updating_button
end
+ end
- # Wait a few seconds for the sync to occur and then refresh the page
- # so that 'last update' shows 'just now' or a period in seconds
- sleep 5
+ def verify_update(url)
refresh
- wait_until(max_duration: 180, sleep_interval: 1) do
+ row_index = find_repository_row_index(url)
+
+ wait_until(sleep_interval: 1) do
within_element_by_index(:mirrored_repository_row, row_index) do
last_update = find_element(:mirror_last_update_at_cell, wait: 0)
last_update.has_text?('just now') || last_update.has_text?('seconds')
diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb
index 4c9df2716e2..b234a9ba986 100644
--- a/qa/qa/page/project/show.rb
+++ b/qa/qa/page/project/show.rb
@@ -121,6 +121,8 @@ module QA
end
def has_file?(name)
+ return false unless has_element?(:file_tree_table)
+
within_element(:file_tree_table) do
has_element?(:file_name_link, text: name)
end
diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb
index 403c919c6e5..435cc4a717e 100644
--- a/qa/qa/page/project/web_ide/edit.rb
+++ b/qa/qa/page/project/web_ide/edit.rb
@@ -32,7 +32,7 @@ module QA
element :file_template_dropdown
end
- view 'app/assets/javascripts/ide/components/file_templates/dropdown.vue' do
+ view 'app/assets/javascripts/ide/components/file_templates/bar.vue' do
element :dropdown_filter_input
end
diff --git a/qa/qa/page/trials/new.rb b/qa/qa/page/trials/new.rb
index cd3b145a89e..40f593a7aa7 100644
--- a/qa/qa/page/trials/new.rb
+++ b/qa/qa/page/trials/new.rb
@@ -12,7 +12,7 @@ module QA
select :number_of_employees
text_field :telephone_number
select :country
- select :state, id: 'state'
+ select :state
button :continue
end
end
diff --git a/qa/qa/page/trials/select.rb b/qa/qa/page/trials/select.rb
index 3da0fb46322..39ef604a781 100644
--- a/qa/qa/page/trials/select.rb
+++ b/qa/qa/page/trials/select.rb
@@ -6,12 +6,11 @@ module QA
class Select < Chemlab::Page
path '/-/trials/select'
- # TODO: Supplant with data-qa-selectors
- select :subscription_for, id: 'namespace_id'
- text_field :new_group_name, id: 'new_group_name'
- button :start_your_free_trial, value: 'Start your free trial'
- radio :trial_company, id: 'trial_entity_company'
- radio :trial_individual, id: 'trial_entity_individual'
+ select :subscription_for
+ text_field :new_group_name
+ button :start_your_free_trial
+ radio :trial_company
+ radio :trial_individual
end
end
end
diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb
index 1958884916c..79cb1ebebc9 100644
--- a/qa/qa/resource/api_fabricator.rb
+++ b/qa/qa/resource/api_fabricator.rb
@@ -54,7 +54,7 @@ module QA
body)
unless response.code == HTTP_STATUS_OK
- raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
+ raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
end
process_api_response(parse_body(response))
@@ -91,9 +91,9 @@ module QA
response = get(request.url)
if response.code == HTTP_STATUS_SERVER_ERROR
- raise InternalServerError, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`."
+ raise InternalServerError, "Failed to GET #{request.mask_url} - (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
elsif response.code != HTTP_STATUS_OK
- raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`."
+ raise ResourceNotFoundError, "Resource at #{request.mask_url} could not be found (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
end
@api_fabrication_http_method = :get # rubocop:disable Gitlab/ModuleWithInstanceVariables
@@ -114,6 +114,7 @@ module QA
unless graphql_response.code == HTTP_STATUS_OK && (body[:errors].nil? || body[:errors].empty?)
raise(ResourceFabricationFailedError, <<~MSG)
Fabrication of #{self.class.name} using the API failed (#{graphql_response.code}) with `#{graphql_response}`.
+ #{QA::Support::Loglinking.failure_metadata(graphql_response.headers[:x_request_id])}
MSG
end
@@ -126,7 +127,7 @@ module QA
unless response.code == HTTP_STATUS_CREATED
raise(
ResourceFabricationFailedError,
- "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
+ "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
)
end
@@ -145,7 +146,7 @@ module QA
response = delete(request.url)
unless [HTTP_STATUS_NO_CONTENT, HTTP_STATUS_ACCEPTED].include? response.code
- raise ResourceNotDeletedError, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`."
+ raise ResourceNotDeletedError, "Resource at #{request.mask_url} could not be deleted (#{response.code}): `#{response}`.\n#{QA::Support::Loglinking.failure_metadata(response.headers[:x_request_id])}"
end
response
@@ -165,6 +166,14 @@ module QA
def transform_api_resource(api_resource)
api_resource
end
+
+ # Get api request url
+ #
+ # @param [String] path
+ # @return [String]
+ def request_url(path, **opts)
+ Runtime::API::Request.new(api_client, path, **opts).url
+ end
end
end
end
diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb
index 26355729dab..c06671be77d 100644
--- a/qa/qa/resource/deploy_key.rb
+++ b/qa/qa/resource/deploy_key.rb
@@ -5,6 +5,8 @@ module QA
class DeployKey < Base
attr_accessor :title, :key
+ attribute :id
+
attribute :md5_fingerprint do
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |key|
@@ -34,6 +36,46 @@ module QA
end
end
end
+
+ def fabricate_via_api!
+ resource_web_url(api_get)
+ rescue ResourceNotFoundError
+ super
+ end
+
+ def resource_web_url(resource)
+ super
+ rescue ResourceURLMissingError
+ # this particular resource does not expose a web_url property
+ end
+
+ def api_get_path
+ "/projects/#{project.id}/deploy_keys/#{find_id}"
+ end
+
+ def api_post_path
+ "/projects/#{project.id}/deploy_keys"
+ end
+
+ def api_post_body
+ {
+ key: key,
+ title: title
+ }
+ end
+
+ private
+
+ def find_id
+ id
+ rescue NoValueError
+ found_key = auto_paginated_response(request_url("/projects/#{project.id}/deploy_keys", per_page: '100'))
+ .find { |keys| keys[:key].strip == @key.strip }
+
+ return found_key.fetch(:id) if found_key
+
+ raise ResourceNotFoundError
+ end
end
end
end
diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb
index d60b90b534f..0e6dd626312 100644
--- a/qa/qa/resource/fork.rb
+++ b/qa/qa/resource/fork.rb
@@ -95,7 +95,7 @@ module QA
def wait_until_forked
Runtime::Logger.debug("Waiting for the fork process to complete...")
forked = wait_until do
- project.import_status == "finished"
+ project.reload!.import_status == "finished"
end
raise "Timed out while waiting for the fork process to complete." unless forked
diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb
index dee63f9699c..c3da9d47de5 100644
--- a/qa/qa/resource/group.rb
+++ b/qa/qa/resource/group.rb
@@ -10,7 +10,11 @@ module QA
end
attribute :name do
- @name || path
+ @name || @path || Runtime::Namespace.name
+ end
+
+ attribute :path do
+ @path || @name || Runtime::Namespace.name
end
attribute :sandbox do
@@ -20,7 +24,6 @@ module QA
end
def initialize
- @path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}"
@require_two_factor_authentication = false
end
@@ -64,7 +67,7 @@ module QA
{
parent_id: sandbox.id,
path: path,
- name: name || path,
+ name: name,
visibility: 'public',
require_two_factor_authentication: @require_two_factor_authentication,
avatar: avatar
diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb
index 05b41a4b4f6..889197a0373 100644
--- a/qa/qa/resource/group_base.rb
+++ b/qa/qa/resource/group_base.rb
@@ -64,6 +64,10 @@ module QA
end
end
+ def marked_for_deletion?
+ reload!.api_response[:marked_for_deletion_on].present?
+ end
+
# Get group badges
#
# @return [Array<QA::Resource::GroupBadge>]
@@ -80,22 +84,6 @@ module QA
end
end
- # Get group members
- #
- # @return [Array<QA::Resource::User>]
- def members
- parse_body(api_get_from("#{api_get_path}/members")).map do |member|
- User.init do |resource|
- resource.api_client = api_client
- resource.id = member[:id]
- resource.name = member[:name]
- resource.username = member[:username]
- resource.email = member[:email]
- resource.access_level = member[:access_level]
- end
- end
- end
-
# API get path
#
# @return [String]
diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
index 0a63ff47694..0443b26064e 100644
--- a/qa/qa/resource/kubernetes_cluster/project_cluster.rb
+++ b/qa/qa/resource/kubernetes_cluster/project_cluster.rb
@@ -26,9 +26,6 @@ module QA
Page::Project::Infrastructure::Kubernetes::Index.perform(
&:connect_existing_cluster)
- Page::Project::Infrastructure::Kubernetes::Add.perform(
- &:add_existing_cluster)
-
Page::Project::Infrastructure::Kubernetes::AddExisting.perform do |cluster_page|
cluster_page.set_cluster_name(@cluster.cluster_name)
cluster_page.set_api_url(@cluster.api_url)
diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb
index c5b72eebe03..740a8920cf2 100644
--- a/qa/qa/resource/project.rb
+++ b/qa/qa/resource/project.rb
@@ -262,7 +262,7 @@ module QA
reload!.api_response[:default_branch] || Runtime::Env.default_branch
end
- def import_status
+ def project_import_status
response = get(request_url("/projects/#{id}/import"))
unless response.code == HTTP_STATUS_OK
@@ -276,7 +276,7 @@ module QA
Runtime::Logger.error("Failed relations: #{result[:failed_relations]}")
end
- result[:import_status]
+ result
end
def commits(auto_paginate: false, attempts: 0)
@@ -373,6 +373,30 @@ module QA
api_post_to(api_wikis_path, title: title, content: content)
end
+ # Uses the API to wait until a pull mirroring update is successful (pull mirroring is treated as an import)
+ def wait_for_pull_mirroring
+ mirror_succeeded = Support::Retrier.retry_until(max_duration: 180, raise_on_failure: false, sleep_interval: 1) do
+ reload!
+ api_resource[:import_status] == "finished"
+ end
+
+ unless mirror_succeeded
+ raise "Mirroring failed with error: #{api_resource[:import_error]}"
+ end
+ end
+
+ def remove_via_api!
+ super
+
+ Support::Retrier.retry_until(max_duration: 60, sleep_interval: 1, message: "Waiting for #{self.class.name} to be removed") do
+ !exists?
+ rescue InternalServerError
+ # Retry on transient errors that are likely to be due to race conditions between concurrent delete operations
+ # when parts of a resource are stored in multiple tables
+ false
+ end
+ end
+
protected
# Return subset of fields for comparing projects
@@ -407,14 +431,6 @@ module QA
Git::Location.new(api_resource[:http_url_to_repo])
api_resource
end
-
- # Get api request url
- #
- # @param [String] path
- # @return [String]
- def request_url(path, **opts)
- Runtime::API::Request.new(api_client, path, **opts).url
- end
end
end
end
diff --git a/qa/qa/resource/protected_branch.rb b/qa/qa/resource/protected_branch.rb
index 062d4e9f3d8..55ad6edb3c1 100644
--- a/qa/qa/resource/protected_branch.rb
+++ b/qa/qa/resource/protected_branch.rb
@@ -72,6 +72,16 @@ module QA
self.remove_via_api!(&block)
end
+ # Remove the branch protection after confirming that it exists
+ def remove_via_api!
+ Support::Retrier.retry_until(max_duration: 60, sleep_interval: 1, message: "Waiting for branch #{branch_name} to be protected") do
+ # We confirm it exists before removal because there's no creation event when the default branch is automatically protected by GitLab itself, and there's a slight delay between creating the repo and protecting the default branch
+ exists?
+ end
+
+ super
+ end
+
def api_get_path
"/projects/#{project.id}/protected_branches/#{branch_name}"
end
diff --git a/qa/qa/resource/reusable_collection.rb b/qa/qa/resource/reusable_collection.rb
index 1168b0091fc..99a55800d1c 100644
--- a/qa/qa/resource/reusable_collection.rb
+++ b/qa/qa/resource/reusable_collection.rb
@@ -35,6 +35,10 @@ module QA
instance.each_resource do |reuse_as, resource|
next QA::Runtime::Logger.debug("#{resource.class.name} reused as :#{reuse_as} has already been removed.") unless resource.exists?
+ if resource.respond_to?(:marked_for_deletion?) && resource.marked_for_deletion?
+ next QA::Runtime::Logger.debug("#{resource.class.name} reused as :#{reuse_as} is already scheduled to be removed.")
+ end
+
resource.method(:remove_via_api!).super_method.call
end
end
diff --git a/qa/qa/resource/reusable_group.rb b/qa/qa/resource/reusable_group.rb
index b75cb0517bf..05ff38249f6 100644
--- a/qa/qa/resource/reusable_group.rb
+++ b/qa/qa/resource/reusable_group.rb
@@ -8,7 +8,7 @@ module QA
def initialize
super
- @name = @path = 'reusable_group'
+ @name = @path = QA::Runtime::Env.reusable_group_path
@description = "QA reusable group"
@reuse_as = :default_group
end
diff --git a/qa/qa/resource/reusable_project.rb b/qa/qa/resource/reusable_project.rb
index b9fca314122..8a12c25b6f0 100644
--- a/qa/qa/resource/reusable_project.rb
+++ b/qa/qa/resource/reusable_project.rb
@@ -15,7 +15,7 @@ module QA
super
@add_name_uuid = false
- @name = @path = 'reusable_project'
+ @name = @path = QA::Runtime::Env.reusable_project_path
@reuse_as = :default_project
@initialize_with_readme = true
end
diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb
index e69702a8ffa..9c5c9992442 100644
--- a/qa/qa/resource/runner.rb
+++ b/qa/qa/resource/runner.rb
@@ -47,9 +47,8 @@ module QA
def remove_via_api!
runners = project.runners(tag_list: @tags)
- unless runners && !runners.empty?
- raise "Project #{project.path_with_namespace} has no runners#{" with tags #{@tags}." if @tags&.any?}"
- end
+
+ return if runners.blank?
this_runner = runners.find { |runner| runner[:description] == name }
unless this_runner
diff --git a/qa/qa/runtime/allure_report.rb b/qa/qa/runtime/allure_report.rb
index 9ae04dbe111..10f47ca56ba 100644
--- a/qa/qa/runtime/allure_report.rb
+++ b/qa/qa/runtime/allure_report.rb
@@ -74,8 +74,8 @@ module QA
# @return [void]
def configure_rspec
RSpec.configure do |config|
- config.add_formatter(AllureRspecFormatter)
config.add_formatter(QA::Support::Formatters::AllureMetadataFormatter)
+ config.add_formatter(AllureRspecFormatter)
config.append_after do |example|
Allure.add_attachment(
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index b5b572890c1..5ca3b0c51f8 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -8,12 +8,11 @@ module QA
AuthorizationError = Class.new(RuntimeError)
- def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true, user: nil, ip_limits: false)
+ def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true, user: nil)
@address = address
@personal_access_token = personal_access_token
@is_new_session = is_new_session
@user = user
- enable_ip_limits if ip_limits
end
# Personal access token
@@ -68,24 +67,6 @@ module QA
private
- def enable_ip_limits
- Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
-
- Runtime::Browser.visit(@address, Page::Main::Login)
- Page::Main::Login.perform(&:sign_in_using_admin_credentials)
- Page::Main::Menu.perform(&:go_to_admin_area)
- Page::Admin::Menu.perform(&:go_to_network_settings)
-
- Page::Admin::Settings::Network.perform do |setting|
- setting.expand_ip_limits do |page|
- page.enable_throttles
- page.save_settings
- end
- end
-
- Page::Main::Menu.perform(&:sign_out)
- end
-
# Create PAT
#
# Use api if admin personal access token is present and skip any UI actions otherwise perform creation via UI
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 088822cc2ca..63207751c78 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -404,6 +404,14 @@ module QA
ENV.fetch('GITLAB_QA_LOOP_RUNNER_MINUTES', 1).to_i
end
+ def reusable_project_path
+ ENV.fetch('QA_REUSABLE_PROJECT_PATH', 'reusable_project')
+ end
+
+ def reusable_group_path
+ ENV.fetch('QA_REUSABLE_GROUP_PATH', 'reusable_group')
+ end
+
def mailhog_hostname
ENV['MAILHOG_HOSTNAME']
end
@@ -441,11 +449,6 @@ module QA
running_in_ci? && enabled?(ENV['QA_EXPORT_TEST_METRICS'], default: true)
end
- def test_resources_created_filepath
- file_name = running_in_ci? ? "test-resources-#{SecureRandom.hex(3)}.json" : 'test-resources.json'
- ENV.fetch('QA_TEST_RESOURCES_CREATED_FILEPATH', File.join(Path.qa_root, 'tmp', file_name))
- end
-
def ee_activation_code
ENV['QA_EE_ACTIVATION_CODE']
end
@@ -458,6 +461,10 @@ module QA
enabled?(ENV['QA_VALIDATE_RESOURCE_REUSE'], default: false)
end
+ def skip_smoke_reliable?
+ enabled?(ENV['QA_SKIP_SMOKE_RELIABLE'], default: false)
+ end
+
private
def remote_grid_credentials
diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb
index 81c41000033..1f17146303a 100644
--- a/qa/qa/runtime/logger.rb
+++ b/qa/qa/runtime/logger.rb
@@ -1,33 +1,19 @@
# frozen_string_literal: true
-require 'logger'
require 'forwardable'
-require 'rainbow/refinement'
module QA
module Runtime
- module Logger
+ class Logger
extend SingleForwardable
- using Rainbow
def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
- singleton_class.module_eval do
- attr_writer :logger
-
- def logger
- Rainbow.enabled = Runtime::Env.colorized_logs?
-
- @logger ||= ::Logger.new(Runtime::Env.log_destination).tap do |logger|
- logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR
-
- logger.formatter = proc do |severity, datetime, progname, msg|
- date_format = datetime.strftime("%Y-%m-%d %H:%M:%S")
-
- "[date=#{date_format} from=QA Tests] #{severity.ljust(5)} -- ".yellow + "#{msg}\n"
- end
- end
- end
+ def self.logger
+ @logger ||= Gitlab::QA::TestLogger.logger(
+ level: Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::INFO,
+ source: 'QA Tests'
+ )
end
end
end
diff --git a/qa/qa/scenario/test/integration/object_storage_gcs.rb b/qa/qa/scenario/test/integration/object_storage_gcs.rb
new file mode 100644
index 00000000000..81e56c9316f
--- /dev/null
+++ b/qa/qa/scenario/test/integration/object_storage_gcs.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class ObjectStorageGcs < Test::Instance::All
+ tags :object_storage
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/integration/registry_with_cdn.rb b/qa/qa/scenario/test/integration/registry_with_cdn.rb
new file mode 100644
index 00000000000..87114d3c203
--- /dev/null
+++ b/qa/qa/scenario/test/integration/registry_with_cdn.rb
@@ -0,0 +1,14 @@
+# rubocop:todo Naming/FileName
+# frozen_string_literal: true
+
+module QA
+ module Scenario
+ module Test
+ module Integration
+ class RegistryWithCDN < Test::Instance::All
+ tags :registry
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb
index 8ffb7c47652..c364b00629c 100644
--- a/qa/qa/service/praefect_manager.rb
+++ b/qa/qa/service/praefect_manager.rb
@@ -327,6 +327,22 @@ module QA
end
end
+ def accept_dataloss_for_project(project_id, authoritative_storage)
+ repository_hash = "#{Digest::SHA256.hexdigest(project_id.to_s)}"
+ repository = "@hashed/#{repository_hash[0, 2]}/#{repository_hash[2, 2]}/#{repository_hash}.git"
+
+ cmd = %{
+ docker exec #{@praefect} \
+ praefect \
+ -config /var/opt/gitlab/praefect/config.toml \
+ accept-dataloss \
+ --virtual-storage=default \
+ --repository=#{repository} \
+ --authoritative-storage=#{authoritative_storage}
+ }
+ shell(cmd)
+ end
+
def wait_for_health_check_all_nodes
wait_for_gitaly_health_check(@primary_node)
wait_for_gitaly_health_check(@secondary_node)
@@ -415,6 +431,27 @@ module QA
Support::Waiter.wait_until(sleep_interval: 1) { replication_queue_incomplete_count == 0 && replicated?(project_id) }
end
+ def wait_for_replication_to_node(project_id, node)
+ Support::Waiter.wait_until(sleep_interval: 1) do
+ result = []
+ shell sql_to_docker_exec_cmd(%{
+ select * from replication_queue
+ where state = 'ready'
+ and job ->> 'change' = 'update'
+ and job ->> 'target_node_storage' = '#{node}'
+ and job ->> 'relative_path' = '#{Digest::SHA256.hexdigest(project_id.to_s)}.git';
+ }) do |line|
+ result << line.strip
+ QA::Runtime::Logger.debug(line.strip)
+ end
+ # The result should look like this when all items are replicated
+ # id | state | created_at | updated_at | attempt | lock_id | job | meta
+ # ----+-------+------------+------------+---------+---------+-----+------
+ # (0 rows)
+ result[2] == '(0 rows)'
+ end
+ end
+
def replication_pending?
result = []
shell sql_to_docker_exec_cmd(
diff --git a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
index a51d733d484..c39db63f64d 100644
--- a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage', :github, :requires_admin, :reliable do
- describe 'Project import' do
+ describe 'Project import', issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/353583' do
let!(:api_client) { Runtime::API::Client.as_admin }
let!(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
let!(:user) do
@@ -17,7 +17,7 @@ module QA
project.name = 'imported-project'
project.group = group
project.github_personal_access_token = Runtime::Env.github_access_token
- project.github_repository_path = 'gitlab-qa-github/test-project'
+ project.github_repository_path = 'gitlab-qa-github/import-test'
project.api_client = api_client
end
end
@@ -31,11 +31,13 @@ module QA
end
it 'imports Github repo via api', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347670' do
- imported_project # import the project
+ imported_project.reload! # import the project
- expect { imported_project.reload!.import_status }.to eventually_eq('finished').within(max_duration: 90)
+ expect { imported_project.project_import_status[:import_status] }.to eventually_eq('finished')
+ .within(max_duration: 90, sleep_interval: 1)
aggregate_failures do
+ verify_status_data
verify_repository_import
verify_commits_import
verify_labels_import
@@ -46,15 +48,29 @@ module QA
end
end
+ def verify_status_data
+ stats = imported_project.project_import_status.dig(:stats, :imported)
+ expect(stats).to include(
+ # issue: 2,
+ label: 9,
+ milestone: 1,
+ note: 3,
+ pull_request: 1,
+ pull_request_review: 1,
+ diff_note: 1,
+ release: 1
+ )
+ end
+
def verify_repository_import
expect(imported_project.api_response).to include(
- description: 'A new repo for test',
+ description: 'Project for github import test',
import_error: nil
)
end
def verify_commits_import
- expect(imported_project.commits.length).to eq(20)
+ expect(imported_project.commits.length).to eq(2)
end
def verify_labels_import
@@ -62,7 +78,6 @@ module QA
expect(labels).to include(
{ name: 'bug', color: '#d73a4a' },
- { name: 'custom new label', color: '#fc8f91' },
{ name: 'documentation', color: '#0075ca' },
{ name: 'duplicate', color: '#cfd3d7' },
{ name: 'enhancement', color: '#a2eeef' },
@@ -79,10 +94,10 @@ module QA
expect(issues.length).to eq(1)
expect(issues.first).to include(
- title: 'This is a sample issue',
- description: "*Created by: gitlab-qa-github*\n\nThis is a sample first comment",
- labels: ['custom new label', 'good first issue', 'help wanted'],
- user_notes_count: 1
+ title: 'Test issue',
+ description: "*Created by: gitlab-qa-github*\n\nTest issue description",
+ labels: ['good first issue', 'help wanted', 'question'],
+ user_notes_count: 2
)
end
@@ -90,7 +105,7 @@ module QA
milestones = imported_project.milestones
expect(milestones.length).to eq(1)
- expect(milestones.first).to include(title: 'v1.0', description: nil, state: 'active')
+ expect(milestones.first).to include(title: '0.0.1', description: nil, state: 'active')
end
def verify_wikis_import
@@ -111,20 +126,20 @@ module QA
expect(merge_requests.length).to eq(1)
expect(merge_request.api_resource).to include(
- title: 'Improve readme',
+ title: 'Test pull request',
state: 'opened',
target_branch: 'main',
- source_branch: 'improve-readme',
- labels: %w[bug documentation],
+ source_branch: 'gitlab-qa-github-patch-1',
+ labels: %w[documentation],
description: <<~DSC.strip
- *Created by: gitlab-qa-github*\n\nThis improves the README file a bit.\r\n\r\nTODO:\r\n\r\n \r\n\r\n- [ ] Do foo\r\n- [ ] Make bar\r\n - [ ] Think about baz
+ *Created by: gitlab-qa-github*\n\nTest pull request body
DSC
)
- expect(mr_comments).to eq(
+ expect(mr_comments).to match_array(
[
- "*Created by: gitlab-qa-github*\n\n[PR comment by @sliaquat] Nice work! ",
- "*Created by: gitlab-qa-github*\n\n[Single diff comment] Nice addition",
- "*Created by: gitlab-qa-github*\n\n[Single diff comment] Good riddance"
+ "*Created by: gitlab-qa-github*\n\n**Review:** Commented\n\nGood but needs some improvement",
+ "*Created by: gitlab-qa-github*\n\n```suggestion:-0+0\nProject for GitHub import test to GitLab\r\n```",
+ "*Created by: gitlab-qa-github*\n\nSome test PR comment"
]
)
end
diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
index c46de0ac514..84eda023576 100644
--- a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb
@@ -3,11 +3,16 @@
# rubocop:disable Rails/Pluck
module QA
# Only executes in custom job/pipeline
+ # https://gitlab.com/gitlab-org/manage/import/import-github-performance
+ #
RSpec.describe 'Manage', :github, :requires_admin, only: { job: 'large-github-import' } do
describe 'Project import' do
let(:logger) { Runtime::Logger.logger }
let(:differ) { RSpec::Support::Differ.new(color: true) }
+ let(:created_by_pattern) { /\*Created by: \S+\*\n\n/ }
+ let(:suggestion_pattern) { /suggestion:-\d+\+\d+/ }
+
let(:api_client) { Runtime::API::Client.as_admin }
let(:user) do
@@ -19,46 +24,57 @@ module QA
let(:github_repo) { ENV['QA_LARGE_GH_IMPORT_REPO'] || 'rspec/rspec-core' }
let(:import_max_duration) { ENV['QA_LARGE_GH_IMPORT_DURATION'] ? ENV['QA_LARGE_GH_IMPORT_DURATION'].to_i : 7200 }
let(:github_client) do
- Octokit.middleware = Faraday::RackBuilder.new do |builder|
- builder.response(:logger, logger, headers: false, bodies: false)
- end
-
Octokit::Client.new(
access_token: ENV['QA_LARGE_GH_IMPORT_GH_TOKEN'] || Runtime::Env.github_access_token,
auto_paginate: true
)
end
- let(:gh_branches) { github_client.branches(github_repo).map(&:name) }
- let(:gh_commits) { github_client.commits(github_repo).map(&:sha) }
let(:gh_repo) { github_client.repository(github_repo) }
+ let(:gh_branches) do
+ logger.debug("= Fetching branches =")
+ github_client.branches(github_repo).map(&:name)
+ end
+
+ let(:gh_commits) do
+ logger.debug("= Fetching commits =")
+ github_client.commits(github_repo).map(&:sha)
+ end
+
let(:gh_labels) do
+ logger.debug("= Fetching labels =")
github_client.labels(github_repo).map { |label| { name: label.name, color: "##{label.color}" } }
end
let(:gh_milestones) do
+ logger.debug("= Fetching milestones =")
github_client
.list_milestones(github_repo, state: 'all')
.map { |ms| { title: ms.title, description: ms.description } }
end
let(:gh_all_issues) do
+ logger.debug("= Fetching issues and prs =")
github_client.list_issues(github_repo, state: 'all')
end
let(:gh_prs) do
gh_all_issues.select(&:pull_request).each_with_object({}) do |pr, hash|
- hash[pr.title] = {
+ hash[pr.number] = {
+ url: pr.html_url,
+ title: pr.title,
body: pr.body || '',
- comments: [*gh_pr_comments[pr.html_url], *gh_issue_comments[pr.html_url]].compact.sort
+ comments: [*gh_pr_comments[pr.html_url], *gh_issue_comments[pr.html_url]].compact
}
end
end
let(:gh_issues) do
gh_all_issues.reject(&:pull_request).each_with_object({}) do |issue, hash|
- hash[issue.title] = {
+ hash[issue.number] = {
+ url: issue.html_url,
+ title: issue.title,
body: issue.body || '',
comments: gh_issue_comments[issue.html_url]
}
@@ -66,12 +82,14 @@ module QA
end
let(:gh_issue_comments) do
+ logger.debug("= Fetching issue comments =")
github_client.issues_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key
end
end
let(:gh_pr_comments) do
+ logger.debug("= Fetching pr comments =")
github_client.pull_requests_comments(github_repo).each_with_object(Hash.new { |h, k| h[k] = [] }) do |c, hash|
hash[c.html_url.gsub(/\#\S+/, "")] << c.body # use base html url as key
end
@@ -97,25 +115,34 @@ module QA
"data",
{
import_time: @import_time,
+ reported_stats: @stats,
github: {
project_name: github_repo,
- branches: gh_branches,
- commits: gh_commits,
- labels: gh_labels,
- milestones: gh_milestones,
- prs: gh_prs,
- issues: gh_issues
+ branches: gh_branches.length,
+ commits: gh_commits.length,
+ labels: gh_labels.length,
+ milestones: gh_milestones.length,
+ prs: gh_prs.length,
+ pr_comments: gh_prs.sum { |_k, v| v[:comments].length },
+ issues: gh_issues.length,
+ issue_comments: gh_issues.sum { |_k, v| v[:comments].length }
},
gitlab: {
project_name: imported_project.path_with_namespace,
- branches: gl_branches,
- commits: gl_commits,
- labels: gl_labels,
- milestones: gl_milestones,
- mrs: mrs,
- issues: gl_issues
+ branches: gl_branches.length,
+ commits: gl_commits.length,
+ labels: gl_labels.length,
+ milestones: gl_milestones.length,
+ mrs: mrs.length,
+ mr_comments: mrs.sum { |_k, v| v[:comments].length },
+ issues: gl_issues.length,
+ issue_comments: gl_issues.sum { |_k, v| v[:comments].length }
+ },
+ not_imported: {
+ mrs: @mr_diff,
+ issues: @issue_diff
}
- }.to_json
+ }
)
end
@@ -125,15 +152,25 @@ module QA
) do
start = Time.now
- Runtime::Logger.info("Importing project '#{imported_project.full_path}'") # import the project and log path
- fetch_github_objects # fetch all objects right after import has started
+ # import the project and log gitlab path
+ Runtime::Logger.info("== Importing project '#{github_repo}' in to '#{imported_project.reload!.full_path}' ==")
+ # fetch all objects right after import has started
+ fetch_github_objects
import_status = lambda do
- imported_project.reload!.import_status.tap do |status|
- raise "Import of '#{imported_project.name}' failed!" if status == 'failed'
+ imported_project.project_import_status.yield_self do |status|
+ @stats = status.dig(:stats, :imported)
+
+ # fail fast if import explicitly failed
+ raise "Import of '#{imported_project.name}' failed!" if status[:import_status] == 'failed'
+
+ status[:import_status]
end
end
+
+ logger.info("== Waiting for import to be finished ==")
expect(import_status).to eventually_eq('finished').within(max_duration: import_max_duration, sleep_interval: 30)
+
@import_time = Time.now - start
aggregate_failures do
@@ -149,22 +186,22 @@ module QA
#
# @return [void]
def fetch_github_objects
- logger.debug("== Fetching objects for github repo: '#{github_repo}' ==")
+ logger.info("== Fetching github repo objects ==")
gh_repo
gh_branches
gh_commits
- gh_prs
- gh_issues
gh_labels
gh_milestones
+ gh_prs
+ gh_issues
end
# Verify repository imported correctly
#
# @return [void]
def verify_repository_import
- logger.debug("== Verifying repository import ==")
+ logger.info("== Verifying repository import ==")
expect(imported_project.description).to eq(gh_repo.description)
# check via include, importer creates more branches
# https://gitlab.com/gitlab-org/gitlab/-/issues/332711
@@ -172,82 +209,109 @@ module QA
expect(gl_commits).to match_array(gh_commits)
end
- # Verify imported merge requests and mr issues
+ # Verify imported labels
#
# @return [void]
- def verify_merge_requests_import
- logger.debug("== Verifying merge request import ==")
- verify_mrs_or_issues('mr')
+ def verify_labels_import
+ logger.info("== Verifying label import ==")
+ # check via include, additional labels can be inherited from parent group
+ expect(gl_labels).to include(*gh_labels)
end
- # Verify imported issues and issue comments
+ # Verify milestones import
#
# @return [void]
- def verify_issues_import
- logger.debug("== Verifying issue import ==")
- verify_mrs_or_issues('issue')
+ def verify_milestones_import
+ logger.info("== Verifying milestones import ==")
+ expect(gl_milestones).to match_array(gh_milestones)
end
- # Verify imported labels
+ # Verify imported merge requests and mr issues
#
# @return [void]
- def verify_labels_import
- logger.debug("== Verifying label import ==")
- # check via include, additional labels can be inherited from parent group
- expect(gl_labels).to include(*gh_labels)
+ def verify_merge_requests_import
+ logger.info("== Verifying merge request import ==")
+ @mr_diff = verify_mrs_or_issues('mr')
end
- # Verify milestones import
+ # Verify imported issues and issue comments
#
# @return [void]
- def verify_milestones_import
- logger.debug("== Verifying milestones import ==")
- expect(gl_milestones).to match_array(gh_milestones)
+ def verify_issues_import
+ logger.info("== Verifying issue import ==")
+ @issue_diff = verify_mrs_or_issues('issue')
end
private
- # Verify imported mrs or issues
+ # Verify imported mrs or issues and return missing items
#
# @param [String] type verification object, 'mrs' or 'issues'
- # @return [void]
+ # @return [Hash]
def verify_mrs_or_issues(type)
- msg = ->(title) { "expected #{type} with title '#{title}' to have" }
-
# Compare length to have easy to read overview how many objects are missing
+ #
expected = type == 'mr' ? mrs : gl_issues
actual = type == 'mr' ? gh_prs : gh_issues
count_msg = "Expected to contain same amount of #{type}s. Gitlab: #{expected.length}, Github: #{actual.length}"
expect(expected.length).to eq(actual.length), count_msg
- logger.debug("= Comparing #{type}s =")
- actual.each do |title, actual_item|
- print "." # indicate that it is still going but don't spam the output with newlines
+ missing_comments = verify_comments(type, actual, expected)
- expected_item = expected[title]
+ {
+ "#{type}s": (actual.keys - expected.keys).map { |it| actual[it].slice(:title, :url) },
+ "#{type}_comments": missing_comments
+ }
+ end
+
+ # Verify imported comments
+ #
+ # @param [String] type verification object, 'mrs' or 'issues'
+ # @param [Hash] actual
+ # @param [Hash] expected
+ # @return [Hash]
+ def verify_comments(type, actual, expected)
+ actual.each_with_object([]) do |(key, actual_item), missing_comments|
+ expected_item = expected[key]
+ title = actual_item[:title]
+ msg = "expected #{type} with title '#{title}' to have"
# Print title in the error message to see which object is missing
- expect(expected_item).to be_truthy, "#{msg.call(title)} been imported"
+ #
+ expect(expected_item).to be_truthy, "#{msg} been imported"
next unless expected_item
# Print difference in the description
+ #
expected_body = expected_item[:body]
actual_body = actual_item[:body]
body_msg = <<~MSG
- #{msg.call(title)} same description. diff:\n#{differ.diff(expected_item[:body], actual_item[:body])}
+ #{msg} same description. diff:\n#{differ.diff(expected_body, actual_body)}
MSG
- expect(expected_body).to include(actual_body), body_msg
+ expect(expected_body).to eq(actual_body), body_msg
# Print amount difference first
+ #
expected_comments = expected_item[:comments]
actual_comments = actual_item[:comments]
comment_count_msg = <<~MSG
- #{msg.call(title)} same amount of comments. Gitlab: #{expected_comments.length}, Github: #{actual_comments.length}
+ #{msg} same amount of comments. Gitlab: #{expected_comments.length}, Github: #{actual_comments.length}
MSG
expect(expected_comments.length).to eq(actual_comments.length), comment_count_msg
expect(expected_comments).to match_array(actual_comments)
+
+ # Save missing comments
+ #
+ comment_diff = actual_comments - expected_comments
+ next if comment_diff.empty?
+
+ missing_comments << {
+ title: title,
+ github_url: actual_item[:url],
+ gitlab_url: expected_item[:url],
+ missing_comments: comment_diff
+ }
end
- puts # print newline after last print to make output pretty
end
# Imported project branches
@@ -297,22 +361,27 @@ module QA
@mrs ||= begin
logger.debug("= Fetching merge requests =")
imported_mrs = imported_project.merge_requests(auto_paginate: true, attempts: 2)
- logger.debug("= Transforming merge request objects for comparison =")
- imported_mrs.each_with_object({}) do |mr, hash|
+
+ logger.debug("= Fetching merge request comments =")
+ Parallel.map(imported_mrs, in_threads: 4) do |mr|
resource = Resource::MergeRequest.init do |resource|
resource.project = imported_project
resource.iid = mr[:iid]
resource.api_client = api_client
end
- hash[mr[:title]] = {
- body: mr[:description],
- comments: resource.comments(auto_paginate: true, attempts: 2)
+ logger.debug("Fetching comments for mr '#{mr[:title]}'")
+ [mr[:iid], {
+ url: mr[:web_url],
+ title: mr[:title],
+ body: sanitize_description(mr[:description]) || '',
+ comments: resource
+ .comments(auto_paginate: true, attempts: 2)
# remove system notes
.reject { |c| c[:system] || c[:body].match?(/^(\*\*Review:\*\*)|(\*Merged by:).*/) }
- .map { |c| sanitize(c[:body]) }
- }
- end
+ .map { |c| sanitize_comment(c[:body]) }
+ }]
+ end.to_h
end
end
@@ -323,37 +392,51 @@ module QA
@gl_issues ||= begin
logger.debug("= Fetching issues =")
imported_issues = imported_project.issues(auto_paginate: true, attempts: 2)
- logger.debug("= Transforming issue objects for comparison =")
- imported_issues.each_with_object({}) do |issue, hash|
+
+ logger.debug("= Fetching issue comments =")
+ Parallel.map(imported_issues, in_threads: 4) do |issue|
resource = Resource::Issue.init do |issue_resource|
issue_resource.project = imported_project
issue_resource.iid = issue[:iid]
issue_resource.api_client = api_client
end
- hash[issue[:title]] = {
- body: issue[:description],
- comments: resource.comments(auto_paginate: true, attempts: 2).map { |c| sanitize(c[:body]) }
- }
- end
+ logger.debug("Fetching comments for issue '#{issue[:title]}'")
+ [issue[:iid], {
+ url: issue[:web_url],
+ title: issue[:title],
+ body: sanitize_description(issue[:description]) || '',
+ comments: resource
+ .comments(auto_paginate: true, attempts: 2)
+ .map { |c| sanitize_comment(c[:body]) }
+ }]
+ end.to_h
end
end
- # Remove added prefixes by importer
+ # Remove added prefixes and legacy diff format from comments
+ #
+ # @param [String] body
+ # @return [String]
+ def sanitize_comment(body)
+ body.gsub(created_by_pattern, "").gsub(suggestion_pattern, "suggestion\r")
+ end
+
+ # Remove created by prefix from descripion
#
# @param [String] body
# @return [String]
- def sanitize(body)
- body.gsub(/\*Created by: \S+\*\n\n/, "")
+ def sanitize_description(body)
+ body&.gsub(created_by_pattern, "")
end
# Save json as file
#
# @param [String] name
- # @param [String] json
+ # @param [Hash] json
# @return [void]
def save_json(name, json)
- File.open("tmp/#{name}.json", "w") { |file| file.write(json) }
+ File.open("tmp/#{name}.json", "w") { |file| file.write(JSON.pretty_generate(json)) }
end
end
end
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb
index a6655471591..f721b3326a0 100644
--- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_group_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :requires_admin do
+ RSpec.describe 'Manage', :reliable, :requires_admin do
describe 'Gitlab migration' do
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
let(:admin_api_client) { Runtime::API::Client.as_admin }
@@ -55,9 +55,9 @@ module QA
after do |example|
# Checking for failures in the test currently makes test very flaky due to catching unrelated failures
- # Just log in case of failure until cause of network errors is found
- # See: https://gitlab.com/gitlab-org/gitlab/-/issues/346500
- Runtime::Logger.warn(import_failures) if example.exception && !import_failures.empty?
+ # Log failures for easier debugging
+ Runtime::Logger.warn("Import failures: #{import_failures}") if example.exception && !import_failures.empty?
+ ensure
user.remove_via_api!
end
@@ -147,39 +147,6 @@ module QA
end
end
end
-
- context 'with group members' do
- let(:member) do
- Resource::User.fabricate_via_api! do |usr|
- usr.api_client = admin_api_client
- usr.hard_delete_on_api_removal = true
- end
- end
-
- before do
- member.set_public_email
- source_group.add_member(member, Resource::Members::AccessLevel::DEVELOPER)
-
- imported_group # trigger import
- end
-
- after do
- member.remove_via_api!
- end
-
- it(
- 'adds members for imported group',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347609'
- ) do
- expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration)
-
- imported_member = imported_group.reload!.members.find { |usr| usr.username == member.username }
- aggregate_failures do
- expect(imported_member).not_to be_nil
- expect(imported_member.access_level).to eq(Resource::Members::AccessLevel::DEVELOPER)
- end
- end
- end
end
end
end
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb
new file mode 100644
index 00000000000..704325d9235
--- /dev/null
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_members_spec.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+require_relative 'gitlab_project_migration_common'
+
+module QA
+ RSpec.describe 'Manage' do
+ describe 'Gitlab migration' do
+ include_context 'with gitlab project migration'
+
+ let(:member) do
+ Resource::User.fabricate_via_api! do |usr|
+ usr.api_client = admin_api_client
+ usr.hard_delete_on_api_removal = true
+ end
+ end
+
+ let(:imported_group_member) do
+ imported_group.reload!.list_members.find { |usr| usr['username'] == member.username }
+ end
+
+ let(:imported_project_member) do
+ imported_project.reload!.list_members.find { |usr| usr['username'] == member.username }
+ end
+
+ before do
+ member.set_public_email
+ end
+
+ after do
+ member.remove_via_api!
+ end
+
+ context 'with group member' do
+ before do
+ source_group.add_member(member, Resource::Members::AccessLevel::DEVELOPER)
+ end
+
+ it(
+ 'member retains indirect membership in imported project',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354416'
+ ) do
+ expect_import_finished
+
+ aggregate_failures do
+ expect(imported_project_member).to be_nil
+ expect(imported_group_member&.fetch('access_level')).to eq(
+ Resource::Members::AccessLevel::DEVELOPER
+ )
+ end
+ end
+ end
+
+ context 'with project member' do
+ before do
+ source_project.add_member(member, Resource::Members::AccessLevel::DEVELOPER)
+ end
+
+ it(
+ 'member retains direct membership in imported project',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354417'
+ ) do
+ expect_import_finished
+
+ aggregate_failures do
+ expect(imported_group_member).to be_nil
+ expect(imported_project_member&.fetch('access_level')).to eq(
+ Resource::Members::AccessLevel::DEVELOPER
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb
new file mode 100644
index 00000000000..484c32956e3
--- /dev/null
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_pipeline_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require_relative 'gitlab_project_migration_common'
+
+module QA
+ RSpec.describe 'Manage' do
+ describe 'Gitlab migration' do
+ include_context 'with gitlab project migration'
+
+ context 'with ci pipeline' do
+ let!(:source_project_with_readme) { true }
+
+ let(:source_pipelines) do
+ source_project.pipelines.map do |pipeline|
+ pipeline.except(:id, :web_url, :project_id)
+ end
+ end
+
+ let(:imported_pipelines) do
+ imported_project.pipelines.map do |pipeline|
+ pipeline.except(:id, :web_url, :project_id)
+ end
+ end
+
+ before do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.api_client = api_client
+ commit.project = source_project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files(
+ [
+ {
+ file_path: '.gitlab-ci.yml',
+ content: <<~YML
+ test-success:
+ script: echo 'OK'
+ YML
+ }
+ ]
+ )
+ end
+
+ Support::Waiter.wait_until(max_duration: 10, sleep_interval: 1) do
+ !source_project.pipelines.empty?
+ end
+ end
+
+ it(
+ 'successfully imports ci pipeline',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354650'
+ ) do
+ expect_import_finished
+
+ expect(imported_pipelines).to eq(source_pipelines)
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb
index b7f0a10c525..70f19e9f3d7 100644
--- a/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb
+++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb
@@ -1,13 +1,9 @@
# frozen_string_literal: true
module QA
- # Disable on staging until bulk_import_projects toggle is on by default
+ # Disable on live envs until bulk_import_projects toggle is on by default
# Otherwise tests running in parallel can disable feature in the middle of other test
- RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging }, quarantine: {
- only: { job: 'praefect' },
- type: :investigating,
- issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348999'
- } do
+ RSpec.shared_context 'with gitlab project migration', :requires_admin, :skip_live_env do
let(:source_project_with_readme) { false }
let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } }
let(:admin_api_client) { Runtime::API::Client.as_admin }
@@ -79,13 +75,11 @@ module QA
end
after do |example|
- # Checking for failures in the test currently makes test very flaky
- # Just log in case of failure until cause of network errors is found
+ # Checking for failures in the test currently makes test very flaky due to catching unrelated failures
+ # Log failures for easier debugging
Runtime::Logger.warn("Import failures: #{import_failures}") if example.exception && !import_failures.empty?
-
- user.remove_via_api!
ensure
- Runtime::Feature.disable(:bulk_import_projects)
+ user.remove_via_api!
end
end
end
diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb
index 17ffb901e5a..fc221c963b1 100644
--- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb
@@ -1,19 +1,41 @@
# frozen_string_literal: true
-require 'airborne'
-
module QA
- RSpec.describe 'Manage with IP rate limits', :requires_admin, :skip_live_env do
- describe 'Users API' do
- let(:api_client) { Runtime::API::Client.new(:gitlab, ip_limits: true) }
- let(:request) { Runtime::API::Request.new(api_client, '/users') }
+ RSpec.describe 'Manage', :requires_admin, :skip_live_env, except: { job: 'review-qa-*' } do
+ describe 'rate limits' do
+ let(:rate_limited_user) { Resource::User.fabricate_via_api! }
+ let(:api_client) { Runtime::API::Client.new(:gitlab, user: rate_limited_user) }
+ let!(:request) { Runtime::API::Request.new(api_client, '/users') }
+
+ after do
+ rate_limited_user.remove_via_api!
+ end
+
+ it 'throttles authenticated api requests by user', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347881' do
+ with_application_settings(
+ throttle_authenticated_api_requests_per_period: 5,
+ throttle_authenticated_api_period_in_seconds: 60,
+ throttle_authenticated_api_enabled: true
+ ) do
+ 5.times do
+ res = RestClient.get request.url
+ expect(res.code).to be(200)
+ end
- it 'GET /users', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347881' do
- 5.times do
- get request.url
- expect_status(200)
+ expect { RestClient.get request.url }.to raise_error do |e|
+ expect(e.class).to be(RestClient::TooManyRequests)
+ end
end
end
end
+
+ private
+
+ def with_application_settings(**hargs)
+ QA::Runtime::ApplicationSettings.set_application_settings(**hargs)
+ yield
+ ensure
+ QA::Runtime::ApplicationSettings.restore_application_settings(*hargs.keys)
+ end
end
end
diff --git a/qa/qa/specs/features/api/1_manage/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb
index 53587209bfb..531419e8d0f 100644
--- a/qa/qa/specs/features/api/1_manage/users_spec.rb
+++ b/qa/qa/specs/features/api/1_manage/users_spec.rb
@@ -4,7 +4,7 @@ require 'airborne'
module QA
RSpec.describe 'Manage' do
- describe 'Users API' do
+ describe 'Users API', :reliable do
let(:api_client) { Runtime::API::Client.new(:gitlab) }
let(:request) { Runtime::API::Request.new(api_client, '/users') }
diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb
index 6e2a34afb3e..5b02cc4646c 100644
--- a/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb
+++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb
@@ -52,6 +52,53 @@ module QA
expect(project_data_loss).to include('gitaly3, assigned host, unhealthy')
end
end
+
+ it 'allows admin resolve scenario where data cannot be recovered', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352708' do
+ # Ensure everything is in sync before begining test
+ praefect_manager.wait_for_project_synced_across_all_storages(project.id)
+
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'accept-dataloss-1'
+ commit.add_files([
+ { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly1,gitaly2,gitaly3' }
+ ])
+ end
+
+ praefect_manager.wait_for_replication_to_node(project.id, praefect_manager.primary_node)
+ praefect_manager.stop_primary_node
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'accept-dataloss-2'
+ commit.add_files([
+ { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly2,gitaly3' }
+ ])
+ end
+
+ praefect_manager.wait_for_replication_to_node(project.id, praefect_manager.secondary_node)
+ praefect_manager.stop_secondary_node
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'accept-dataloss-3'
+ commit.add_files([
+ { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'Add a commit to gitaly3' }
+ ])
+ end
+
+ # Confirms that they want to accept dataloss, using gitaly2 as authoritative storage to use as a base
+ praefect_manager.accept_dataloss_for_project(project.id, praefect_manager.secondary_node)
+
+ # Restart nodes, and allow replication to apply dataloss changes
+ praefect_manager.start_all_nodes
+ praefect_manager.wait_for_project_synced_across_all_storages(project.id)
+
+ # Validate that gitaly2 was accepted as the authorative storage
+ aggregate_failures "validate correct set of commits available" do
+ expect(project.commits.map { |commit| commit[:message].chomp }).to include('accept-dataloss-1')
+ expect(project.commits.map { |commit| commit[:message].chomp }).to include('accept-dataloss-2')
+ expect(project.commits.map { |commit| commit[:message].chomp }).not_to include('accept-dataloss-3')
+ end
+ end
end
end
end
diff --git a/qa/qa/specs/features/api/3_create/repository/files_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb
index 48608094f5e..4d28937fbf8 100644
--- a/qa/qa/specs/features/api/3_create/repository/files_spec.rb
+++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb
@@ -104,6 +104,14 @@ module QA
expect(response.headers[:content_disposition]).not_to include("inline")
expect(response.headers[:content_type]).to include("image/svg+xml")
end
+
+ delete_project_request = Runtime::API::Request.new(@api_client, "/projects/#{sanitized_project_path}")
+ delete delete_project_request.url
+
+ expect_status(202)
+ expect(json_body).to match(
+ a_hash_including(message: '202 Accepted')
+ )
end
end
end
diff --git a/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb b/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb
new file mode 100644
index 00000000000..0d10783735b
--- /dev/null
+++ b/qa/qa/specs/features/api/4_verify/remove_runner_spec.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Verify', :runner do
+ describe 'Runner removal' do
+ include Support::API
+
+ let(:api_client) { Runtime::API::Client.new(:gitlab) }
+ let(:executor) { "qa-runner-#{Time.now.to_i}" }
+ let(:runner_tags) { ['runner-registration-e2e-test'] }
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = executor
+ runner.tags = runner_tags
+ end
+ end
+
+ before do
+ sleep 5 # Runner should register within 5 seconds
+ end
+
+ # Removing a runner via the UI is covered by `spec/features/runners_spec.rb``
+ it 'removes the runner', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/355302', type: :investigating } do
+ expect(runner.project.runners.size).to eq(1)
+ expect(runner.project.runners.first[:description]).to eq(executor)
+
+ request = Runtime::API::Request.new(api_client, "runners/#{runner.project.runners.first[:id]}")
+ response = delete(request.url)
+ expect(response.code).to eq(Support::API::HTTP_STATUS_NO_CONTENT)
+ expect(response.body).to be_empty
+
+ expect(runner.project.runners).to be_empty
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb
deleted file mode 100644
index 2db93ac60ea..00000000000
--- a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- RSpec.describe 'Manage' do
- describe 'Subgroup transfer' do
- let(:source_group) do
- Resource::Group.fabricate_via_api! do |group|
- group.path = "source-group-for-transfer_#{SecureRandom.hex(8)}"
- end
- end
-
- let!(:target_group) do
- Resource::Group.fabricate_via_api! do |group|
- group.path = "target-group-for-transfer_#{SecureRandom.hex(8)}"
- end
- end
-
- let(:sub_group_for_transfer) do
- Resource::Group.fabricate_via_api! do |group|
- group.path = "subgroup-for-transfer_#{SecureRandom.hex(8)}"
- group.sandbox = source_group
- end
- end
-
- before do
- Flow::Login.sign_in
- sub_group_for_transfer.visit!
- end
-
- it 'transfers a subgroup to another group',
- testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347692' do
- Page::Group::Menu.perform(&:click_group_general_settings_item)
- Page::Group::Settings::General.perform do |general|
- general.transfer_group(target_group.path, sub_group_for_transfer.path)
-
- sub_group_for_transfer.sandbox = target_group
- sub_group_for_transfer.reload!
- end
-
- expect(page).to have_text("Group '#{sub_group_for_transfer.path}' was successfully transferred.")
- expect(page.driver.current_url).to include(sub_group_for_transfer.full_path)
- end
-
- after do
- source_group&.remove_via_api!
- target_group&.remove_via_api!
- sub_group_for_transfer&.remove_via_api!
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb
index ffd7a7dfb6c..7b60adae836 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_project_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'Project transfer between groups' do
+ describe 'Project transfer between groups', :reliable do
let(:source_group) do
Resource::Group.fabricate_via_api! do |group|
group.path = 'source-group'
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
index 5ba80489652..c86a649f179 100644
--- 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
@@ -29,7 +29,6 @@ module QA
end
before do
- Runtime::Feature.enable(:invite_members_group_modal, group: group)
group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER)
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
index ca0ce0d5775..64614ed654f 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb
@@ -31,7 +31,6 @@ module QA
let(:two_fa_expected_text) { /The group settings for.*require you to enable Two-Factor Authentication for your account.*You need to do this before/ }
before do
- Runtime::Feature.enable(:invite_members_group_modal, group: group)
group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER)
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index bfb810b5c2b..90fbff3261e 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -1,12 +1,8 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :requires_admin do
+ RSpec.describe 'Manage', :reliable do
describe 'Add project member' do
- before do
- Runtime::Feature.enable(:invite_members_group_modal)
- end
-
it 'user adds project member', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347887' do
Flow::Login.sign_in
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
index 72867333d16..d803f5e473c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb
@@ -1,9 +1,9 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Manage', :github, :requires_admin do
+ RSpec.describe 'Manage', :reliable, :github, :requires_admin do
describe 'Project import' do
- let(:github_repo) { 'gitlab-qa-github/test-project' }
+ let(:github_repo) { 'gitlab-qa-github/import-test' }
let(:api_client) { Runtime::API::Client.as_admin }
let(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } }
let(:user) do
@@ -55,7 +55,7 @@ module QA
Page::Project::Show.perform do |project|
aggregate_failures do
expect(project).to have_content(imported_project.name)
- expect(project).to have_content('This test project is used for automated GitHub import by GitLab QA.')
+ expect(project).to have_content('Project for github import test')
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb
index 6997447411a..dd27e85af3c 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb
@@ -2,7 +2,7 @@
module QA
# Tagging with issue for a transient invite group modal search bug, but does not require quarantine at this time
- RSpec.describe 'Manage', :requires_admin, :transient, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/349379' do
+ RSpec.describe 'Manage', :transient, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/349379' do
describe 'Invite group' do
shared_examples 'invites group to project' do
it 'verifies group is added and members can access project with correct access level' do
@@ -16,6 +16,8 @@ module QA
Flow::Login.sign_in(as: @user)
Page::Dashboard::Projects.perform do |projects|
+ projects.filter_by_name(project.name)
+
expect(projects).to have_project_with_access_role(project.name, 'Developer')
end
@@ -28,7 +30,6 @@ module QA
end
before(:context) do
- Runtime::Feature.enable(:invite_members_group_modal)
@user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
end
@@ -79,10 +80,6 @@ module QA
project&.remove_via_api!
group&.remove_via_api!
end
-
- after(:context) do
- Runtime::Feature.disable(:invite_members_group_modal)
- end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb
new file mode 100644
index 00000000000..2aefa1c39ed
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb
@@ -0,0 +1,98 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Manage' do
+ describe 'Personal project permissions' do
+ let!(:owner) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
+
+ let!(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner) }
+
+ let!(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.api_client = owner_api_client
+ project.name = 'qa-owner-personal-project'
+ project.personal_namespace = owner.username
+ end
+ end
+
+ after do
+ project&.remove_via_api!
+ end
+
+ context 'when user is added as Owner' do
+ let(:issue) do
+ Resource::Issue.fabricate_via_api! do |issue|
+ issue.api_client = owner_api_client
+ issue.project = project
+ issue.title = 'Test Owner deletes issue'
+ end
+ end
+
+ before do
+ Flow::Login.sign_in(as: owner)
+ end
+
+ it "has Owner role with Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352542' do
+ Page::Dashboard::Projects.perform do |projects|
+ projects.filter_by_name(project.name)
+
+ expect(projects).to have_project_with_access_role(project.name, 'Owner')
+ end
+
+ expect_owner_permissions_allow_delete_issue
+ end
+ end
+
+ context 'when user is added as Maintainer' do
+ let(:maintainer) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
+
+ let(:issue) do
+ Resource::Issue.fabricate_via_api! do |issue|
+ issue.api_client = owner_api_client
+ issue.project = project
+ issue.title = 'Test Maintainer deletes issue'
+ end
+ end
+
+ before do
+ project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER)
+ Flow::Login.sign_in(as: maintainer)
+ end
+
+ it "has Maintainer role without Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352607' do
+ Page::Dashboard::Projects.perform do |projects|
+ projects.filter_by_name(project.name)
+
+ expect(projects).to have_project_with_access_role(project.name, 'Maintainer')
+ end
+
+ expect_maintainer_permissions_do_not_allow_delete_issue
+ end
+ end
+
+ private
+
+ def expect_owner_permissions_allow_delete_issue
+ expect do
+ issue.visit!
+
+ Page::Project::Issue::Show.perform(&:delete_issue)
+
+ Page::Project::Issue::Index.perform do |index|
+ expect(index).not_to have_issue(issue)
+ end
+ end.not_to raise_error
+ end
+
+ def expect_maintainer_permissions_do_not_allow_delete_issue
+ expect do
+ issue.visit!
+
+ Page::Project::Issue::Show.perform do |issue|
+ expect(issue).not_to have_delete_issue_button
+ end
+ end.not_to raise_error
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb
index 4f9ba579730..f6448fea2d4 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb
@@ -2,8 +2,7 @@
module QA
RSpec.describe 'Manage' do
- # TODO: Remove :requires_admin meta when the `Runtime::Feature.enable` method call is removed
- describe 'Repository tags', :requires_admin do
+ describe 'Repository tags', :reliable do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-for-tags'
@@ -11,12 +10,14 @@ module QA
end
end
- before do
- Runtime::Feature.enable(:invite_members_group_modal, project: project)
+ let(:developer_user) do
+ Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1)
+ end
+
+ let(:maintainer_user) do
+ Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2)
end
- let(:developer_user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
- let(:maintainer_user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
let(:tag_name) { 'v0.0.1' }
let(:tag_message) { 'Version 0.0.1' }
let(:tag_release_notes) { 'Release It!' }
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
index 1be73d92a8c..88f4996ff03 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb
@@ -2,8 +2,9 @@
module QA
RSpec.describe 'Manage' do
- describe 'Project activity' do
- it 'user creates an event in the activity page upon Git push', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347879' do
+ describe 'Project activity', :reliable do
+ it 'user creates an event in the activity page upon Git push',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347879' do
Flow::Login.sign_in
project = Resource::Repository::ProjectPush.fabricate! do |push|
diff --git a/qa/qa/specs/features/browser_ui/1_manage/user/user_access_termination_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/user/user_access_termination_spec.rb
index 58dcd922255..8462f5db30b 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/user/user_access_termination_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/user/user_access_termination_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Manage' do
- describe 'User', :requires_admin do
+ describe 'User', :requires_admin, :reliable do
let(:admin_api_client) { Runtime::API::Client.as_admin }
let!(:user) do
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 4350b8f0d3e..0d706aef6ab 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
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Plan', :orchestrated, :smtp, :requires_admin do
+ RSpec.describe 'Plan', :orchestrated, :smtp do
describe 'Email Notification' do
include Support::API
@@ -16,7 +16,6 @@ module QA
end
before do
- Runtime::Feature.enable(:invite_members_group_modal)
Flow::Login.sign_in
end
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
index c2b42de6701..1ba110a9602 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/check_mentions_for_xss_spec.rb
@@ -4,7 +4,7 @@ module QA
RSpec.describe 'Plan', :reliable do
let!(:user) do
Resource::User.fabricate_via_api! do |user|
- user.name = "eve <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;"
+ user.name = "QA User <img src=x onerror=alert(2)&lt;img src=x onerror=alert(1)&gt;"
user.password = "test1234"
user.api_client = Runtime::API::Client.as_admin
end
@@ -18,8 +18,6 @@ module QA
describe 'check xss occurence in @mentions in issues', :requires_admin do
before do
- Runtime::Feature.enable(:invite_members_group_modal)
-
Flow::Login.sign_in
project.add_member(user)
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 7d6718073f9..e7025920def 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
@@ -1,8 +1,7 @@
# frozen_string_literal: true
module QA
- # TODO: Remove :requires_admin meta when the `Runtime::Feature.enable` method call is removed
- RSpec.describe 'Plan', :smoke, :reliable, :requires_admin do
+ RSpec.describe 'Plan', :smoke, :reliable do
describe 'mention' do
let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let(:project) do
@@ -14,7 +13,6 @@ module QA
before do
Flow::Login.sign_in
- Runtime::Feature.enable(:invite_members_group_modal, project: project)
project.add_member(user)
diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb
index 45d466903de..ac0f16b50cc 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Plan', :requires_admin, :actioncable, :orchestrated do
+ RSpec.describe 'Plan', :requires_admin, :actioncable, :orchestrated, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/293699', type: :bug } do
describe 'Assignees' do
let(:user1) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) }
let(:user2) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) }
@@ -12,22 +12,12 @@ module QA
end
before do
- Runtime::Feature.enable('real_time_issue_sidebar', project: project)
- Runtime::Feature.enable('broadcast_issue_updates', project: project)
- Runtime::Feature.enable(:invite_members_group_modal, project: project)
-
Flow::Login.sign_in
project.add_member(user1)
project.add_member(user2)
end
- after do
- Runtime::Feature.disable('real_time_issue_sidebar', project: project)
- Runtime::Feature.disable('broadcast_issue_updates', project: project)
- Runtime::Feature.disable(:invite_members_group_modal, project: project)
- end
-
it 'update without refresh', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347941' do
issue = Resource::Issue.fabricate_via_api! do |issue|
issue.project = project
diff --git a/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb
index d3662884952..d6eab3c8dd0 100644
--- a/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb
@@ -10,13 +10,13 @@ module QA
let(:group) do
Resource::Group.fabricate_via_api! do |group|
- group.name = 'group-to-test-milestones'
+ group.name = "group-to-test-milestones-#{SecureRandom.hex(4)}"
end
end
let(:project) do
Resource::Project.fabricate_via_api! do |project|
- project.name = 'project-to-test-milestones'
+ project.name = "project-to-test-milestones-#{SecureRandom.hex(4)}"
project.group = group
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 bec95e41202..153bfd292aa 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
@@ -19,7 +19,6 @@ module QA
end
before do
- Runtime::Feature.enable(:invite_members_group_modal, project: parent_project)
parent_project.add_member(user)
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
index b8e425ae3b8..4228c3ed352 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb
@@ -3,7 +3,7 @@
module QA
RSpec.describe 'Create' do
describe 'Push mirror a repository over HTTP' do
- it 'configures and syncs LFS objects for a (push) mirrored repository', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347847' do
+ it 'configures and syncs LFS objects for a (push) mirrored repository', :aggregate_failures, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347847' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.perform(&:sign_in_using_credentials)
@@ -30,14 +30,15 @@ module QA
mirror_settings.authentication_method = 'Password'
mirror_settings.password = Runtime::User.password
mirror_settings.mirror_repository
- mirror_settings.update target_project_uri # rubocop:disable Rails/SaveBang
+ mirror_settings.update(target_project_uri) # rubocop:disable Rails/SaveBang
+ mirror_settings.verify_update(target_project_uri)
end
end
# Check that the target project has the commit from the source
target_project.visit!
Page::Project::Show.perform do |project_page|
- expect(project_page).to have_file('README.md')
+ expect { project_page.has_file?('README.md') }.to eventually_be_truthy.within(max_duration: 60, reload_page: page), "Expected a file named README.md but it did not appear."
expect(project_page).to have_readme_content('The rendered file could not be displayed because it is stored in LFS')
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
index 6d3d86d0663..d644a7ead1e 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_over_http_spec.rb
@@ -29,7 +29,8 @@ module QA
mirror_settings.authentication_method = 'Password'
mirror_settings.password = Runtime::User.password
mirror_settings.mirror_repository
- mirror_settings.update target_project_uri # rubocop:disable Rails/SaveBang
+ mirror_settings.update(target_project_uri) # rubocop:disable Rails/SaveBang
+ mirror_settings.verify_update(target_project_uri)
end
end
@@ -37,7 +38,7 @@ module QA
target_project.visit!
Page::Project::Show.perform do |project|
- expect(project).to have_content('README.md')
+ expect { project.has_file?('README.md') }.to eventually_be_truthy.within(max_duration: 60, reload_page: page), "Expected a file named README.md but it did not appear."
expect(project).to have_content('This is a test project')
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
index 0323448878b..18a77bd5ae3 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'SSH key support' do
+ describe 'SSH key support', :skip_fips_env do
# Note: If you run these tests against GDK make sure you've enabled sshd
# See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb
index 2e8c43d6981..b0eb3ac7b37 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
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'SSH keys support', :smoke do
+ RSpec.describe 'SSH keys support', :smoke, :skip_fips_env do
key_title = "key for ssh tests #{Time.now.to_f}"
key = nil
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 8f22a28628f..7a0b4674581 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
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Version control for personal snippets' do
+ describe 'Version control for personal snippets', :skip_fips_env do
let(:new_file) { 'new_snippet_file' }
let(:changed_content) { 'changes' }
let(:commit_message) { 'Changes to snippets' }
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 9a5fe44c927..d269e02e26d 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
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Create' do
- describe 'Version control for project snippets' do
+ describe 'Version control for project snippets', :skip_fips_env do
let(:new_file) { 'new_snippet_file' }
let(:changed_content) { 'changes' }
let(:commit_message) { 'Changes to snippets' }
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb
index e04f580dc15..b4519327a62 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Create', :smoke do
+ RSpec.describe 'Create', :smoke, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/326624', type: :investigating } do
describe 'Personal snippet creation' do
let(:snippet) do
Resource::Snippet.fabricate_via_browser_ui! do |snippet|
@@ -22,7 +22,7 @@ module QA
end
it 'user creates a personal snippet', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347799' do
- snippet.visit!
+ snippet
Page::Dashboard::Snippet::Show.perform do |snippet|
expect(snippet).to have_snippet_title('Snippet title')
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb
index 28bea89e3bd..ce99822b572 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb
@@ -27,7 +27,7 @@ module QA
end
it 'creates a personal snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347723' do
- snippet.visit!
+ snippet
Page::Dashboard::Snippet::Show.perform do |snippet|
expect(snippet).to have_snippet_title('Personal snippet with multiple files')
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb
index 56cbe7d6bfa..b93bc1545d1 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_spec.rb
@@ -22,7 +22,7 @@ module QA
end
it 'user creates a project snippet', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347798' do
- snippet.visit!
+ snippet
Page::Dashboard::Snippet::Show.perform do |snippet|
expect(snippet).to have_snippet_title('Project snippet')
diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb
index 3d69ef5dde6..70891ec72c7 100644
--- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb
@@ -29,7 +29,7 @@ module QA
end
it 'creates a project snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347725' do
- snippet.visit!
+ snippet
Page::Dashboard::Snippet::Show.perform do |snippet|
aggregate_failures 'file content verification' do
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
index 96e85139e78..653c0657c81 100644
--- 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
@@ -2,7 +2,10 @@
module QA
RSpec.describe 'Create' do
- describe 'Open a fork in Web IDE' do
+ describe 'Open a fork in Web IDE', quarantine: {
+ issue: "https://gitlab.com/gitlab-org/gitlab/-/issues/351696",
+ type: :flaky
+ } do
let(:parent_project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'parent-project'
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb
index 022731faade..09459057992 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/web_terminal_spec.rb
@@ -4,6 +4,8 @@ module QA
RSpec.describe(
'Create',
:runner,
+ # TODO: remove limitation to only run on main when the bug is fixed
+ only: { pipeline: :main },
quarantine: {
issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338179',
type: :bug
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb
index f2ebc191a8a..b9b87ed29bb 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/include_local_config_file_paths_with_wildcard_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Verify' do
- describe 'Include local config file paths with wildcard' do
+ describe 'Include local config file paths with wildcard', :reliable do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-pipeline'
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb
new file mode 100644
index 00000000000..0e7a38626aa
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_can_create_merge_request_spec.rb
@@ -0,0 +1,53 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Verify' do
+ describe 'Pipeline editor' do
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'pipeline-editor-project'
+ project.initialize_with_readme = true
+ end
+ end
+
+ before do
+ Flow::Login.sign_in
+ project.visit!
+ Page::Project::Menu.perform(&:go_to_pipeline_editor)
+ end
+
+ after do
+ project&.remove_via_api!
+ end
+
+ it(
+ 'can create merge request',
+ test_case: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349130'
+ ) do
+ Page::Project::PipelineEditor::New.perform(&:create_new_ci)
+
+ Page::Project::PipelineEditor::Show.perform do |show|
+ # Editor should display default content when project does not have CI file yet
+ # New MR checkbox should not be rendered when a new target branch is yet to be provided
+ aggregate_failures 'check editor default conditions' do
+ expect(show.editing_content).not_to be_empty
+ expect(show).to have_no_new_mr_checkbox
+ end
+
+ # The new MR checkbox is visible after a new target branch name is set
+ show.set_target_branch(SecureRandom.hex(10))
+ expect(show).to have_new_mr_checkbox
+
+ show.select_new_mr_checkbox
+ show.submit_changes
+ end
+
+ Page::MergeRequest::New.perform(&:create_merge_request)
+
+ Page::MergeRequest::Show.perform do |show|
+ expect(show).to have_title('Update .gitlab-ci.yml file')
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
index 8f3284662d7..23f212e110b 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Verify' do
- describe 'Pipeline editor' do
+ describe 'Pipeline editor', :reliable do
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'pipeline-editor-project'
diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb
index 7656aea885e..d1e9981ae74 100644
--- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb
+++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Verify' do
- describe 'Run pipeline' do
+ describe 'Run pipeline', :reliable do
context 'with web only rule' do
let(:job_name) { 'test_job' }
let(:project) do
diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb
index e3c06242a9b..c833aa1a5b8 100644
--- a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb
@@ -1,10 +1,8 @@
# frozen_string_literal: true
module QA
- RSpec.describe 'Package', :orchestrated, only: { pipeline: :main } do
+ RSpec.describe 'Package', :orchestrated, :skip_live_env do
describe 'Self-managed Container Registry' do
- using RSpec::Parameterized::TableSyntax
-
let(:project) do
Resource::Project.fabricate_via_api! do |project|
project.name = 'project-with-registry'
@@ -49,10 +47,63 @@ module QA
end
context "when tls is disabled" do
- where(:authentication_token_type, :token_name) do
- :personal_access_token | 'Personal Access Token'
- :project_deploy_token | 'Deploy Token'
- :ci_job_token | 'Job Token'
+ where do
+ {
+ 'using docker:18.09.9 and a personal access token' => {
+ docker_client_version: 'docker:18.09.9',
+ authentication_token_type: :personal_access_token,
+ token_name: 'Personal Access Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348499'
+ },
+ 'using docker:18.09.9 and a project deploy token' => {
+ docker_client_version: 'docker:18.09.9',
+ authentication_token_type: :project_deploy_token,
+ token_name: 'Deploy Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348852'
+ },
+ 'using docker:18.09.9 and a ci job token' => {
+ docker_client_version: 'docker:18.09.9',
+ authentication_token_type: :ci_job_token,
+ token_name: 'Job Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348765'
+ },
+ 'using docker:19.03.12 and a personal access token' => {
+ docker_client_version: 'docker:19.03.12',
+ authentication_token_type: :personal_access_token,
+ token_name: 'Personal Access Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348507'
+ },
+ 'using docker:19.03.12 and a project deploy token' => {
+ docker_client_version: 'docker:19.03.12',
+ authentication_token_type: :project_deploy_token,
+ token_name: 'Deploy Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348859'
+ },
+ 'using docker:19.03.12 and a ci job token' => {
+ docker_client_version: 'docker:19.03.12',
+ authentication_token_type: :ci_job_token,
+ token_name: 'Job Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348654'
+ },
+ 'using docker:20.10 and a personal access token' => {
+ docker_client_version: 'docker:20.10',
+ authentication_token_type: :personal_access_token,
+ token_name: 'Personal Access Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348754'
+ },
+ 'using docker:20.10 and a project deploy token' => {
+ docker_client_version: 'docker:20.10',
+ authentication_token_type: :project_deploy_token,
+ token_name: 'Deploy Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348856'
+ },
+ 'using docker:20.10 and a ci job token' => {
+ docker_client_version: 'docker:20.10',
+ authentication_token_type: :ci_job_token,
+ token_name: 'Job Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348766'
+ }
+ }
end
with_them do
@@ -78,57 +129,51 @@ module QA
end
end
- where(:docker_client_version) do
- %w[docker:18.09.9 docker:19.03.12 docker:20.10]
- end
-
- with_them do
- it "pushes image and deletes tag", :registry do
- Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- commit.project = project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([{
- file_path: '.gitlab-ci.yml',
- content:
- <<~YAML
- build:
- image: "#{docker_client_version}"
- stage: build
- services:
- - name: "#{docker_client_version}-dind"
- command: ["--insecure-registry=gitlab.test:5050"]
- variables:
- IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
- script:
- - docker login -u #{auth_user} -p #{auth_token} gitlab.test:5050
- - docker build -t $IMAGE_TAG .
- - docker push $IMAGE_TAG
- tags:
- - "runner-for-#{project.name}"
- YAML
- }])
- end
+ it "pushes image and deletes tag", :registry, testcase: params[:testcase] do
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([{
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ build:
+ image: "#{docker_client_version}"
+ stage: build
+ services:
+ - name: "#{docker_client_version}-dind"
+ command: ["--insecure-registry=gitlab.test:5050"]
+ variables:
+ IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
+ script:
+ - docker login -u #{auth_user} -p #{auth_token} gitlab.test:5050
+ - docker build -t $IMAGE_TAG .
+ - docker push $IMAGE_TAG
+ tags:
+ - "runner-for-#{project.name}"
+ YAML
+ }])
end
+ end
- Flow::Pipeline.visit_latest_pipeline
+ Flow::Pipeline.visit_latest_pipeline
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('build')
- end
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('build')
+ end
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
- Page::Project::Menu.perform(&:go_to_container_registry)
+ Page::Project::Menu.perform(&:go_to_container_registry)
- Page::Project::Registry::Show.perform do |registry|
- expect(registry).to have_registry_repository(project.path_with_namespace)
+ Page::Project::Registry::Show.perform do |registry|
+ expect(registry).to have_registry_repository(project.path_with_namespace)
- registry.click_on_image(project.path_with_namespace)
- expect(registry).to have_tag('master')
- end
+ registry.click_on_image(project.path_with_namespace)
+ expect(registry).to have_tag('master')
end
end
end
@@ -156,7 +201,7 @@ module QA
apk add --no-cache openssl
true | openssl s_client -showcerts -connect gitlab.test:5050 > /usr/local/share/ca-certificates/gitlab.test.crt
update-ca-certificates
- dockerd-entrypoint.sh || exit
+ dockerd-entrypoint.sh || exit
variables:
IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG
script:
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb
new file mode 100644
index 00000000000..9ef5b8c84fa
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb
@@ -0,0 +1,314 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Package', :orchestrated, :packages, :object_storage do
+ describe 'Maven group level endpoint' do
+ include Runtime::Fixtures
+ include_context 'packages registry qa scenario'
+
+ let(:group_id) { 'com.gitlab.qa' }
+ let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
+ let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
+ let(:package_version) { '1.3.7' }
+ let(:package_type) { 'maven' }
+
+ context 'via maven' do
+ where do
+ {
+ 'using a personal access token' => {
+ authentication_token_type: :personal_access_token,
+ maven_header_name: 'Private-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347582'
+ },
+ 'using a project deploy token' => {
+ authentication_token_type: :project_deploy_token,
+ maven_header_name: 'Deploy-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347585'
+ },
+ 'using a ci job token' => {
+ authentication_token_type: :ci_job_token,
+ maven_header_name: 'Job-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347579'
+ }
+ }
+ end
+
+ with_them do
+ let(:token) do
+ case authentication_token_type
+ when :personal_access_token
+ personal_access_token
+ when :ci_job_token
+ '${env.CI_JOB_TOKEN}'
+ when :project_deploy_token
+ project_deploy_token.token
+ end
+ end
+
+ it 'pushes and pulls a maven package', testcase: params[:testcase] do
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ maven_upload_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_upload_package.yaml.erb')).result(binding)
+ package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding)
+ settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding)
+
+ commit.project = package_project
+ commit.commit_message = 'Add files'
+ commit.add_files([
+ {
+ file_path: '.gitlab-ci.yml',
+ content: maven_upload_package_yaml
+ },
+ {
+ file_path: 'pom.xml',
+ content: package_pom_xml
+ },
+ {
+ file_path: 'settings.xml',
+ content: settings_xml
+ }
+ ])
+ end
+ end
+
+ package_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('deploy')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+
+ 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, package_version)
+ end
+
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ maven_install_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_install_package.yaml.erb')).result(binding)
+ client_pom_xml = ERB.new(read_fixture('package_managers/maven', 'client_pom.xml.erb')).result(binding)
+ settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding)
+
+ commit.project = client_project
+ commit.commit_message = 'Add files'
+ commit.add_files([
+ {
+ file_path: '.gitlab-ci.yml',
+ content: maven_install_package_yaml
+ },
+ {
+ file_path: 'pom.xml',
+ content: client_pom_xml
+ },
+ {
+ file_path: 'settings.xml',
+ content: settings_xml
+ }
+ ])
+ end
+ end
+
+ client_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('install')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+ end
+ end
+ end
+
+ context 'duplication setting' do
+ before do
+ package_project.group.visit!
+
+ Page::Group::Menu.perform(&:go_to_package_settings)
+ end
+
+ context 'when disabled' do
+ where do
+ {
+ 'using a personal access token' => {
+ authentication_token_type: :personal_access_token,
+ maven_header_name: 'Private-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347581'
+ },
+ 'using a project deploy token' => {
+ authentication_token_type: :project_deploy_token,
+ maven_header_name: 'Deploy-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347584'
+ },
+ 'using a ci job token' => {
+ authentication_token_type: :ci_job_token,
+ maven_header_name: 'Job-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347578'
+ }
+ }
+ end
+
+ with_them do
+ let(:token) do
+ case authentication_token_type
+ when :personal_access_token
+ personal_access_token
+ when :ci_job_token
+ '${env.CI_JOB_TOKEN}'
+ when :project_deploy_token
+ project_deploy_token.token
+ end
+ end
+
+ before do
+ Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled)
+ end
+
+ it 'prevents users from publishing group level Maven packages duplicates', testcase: params[:testcase] do
+ create_duplicated_package
+
+ push_duplicated_package
+
+ client_project.visit!
+
+ show_latest_deploy_job
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).not_to be_successful(timeout: 800)
+ end
+ end
+ end
+ end
+
+ context 'when enabled' do
+ where do
+ {
+ 'using a personal access token' => {
+ authentication_token_type: :personal_access_token,
+ maven_header_name: 'Private-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347580'
+ },
+ 'using a project deploy token' => {
+ authentication_token_type: :project_deploy_token,
+ maven_header_name: 'Deploy-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347583'
+ },
+ 'using a ci job token' => {
+ authentication_token_type: :ci_job_token,
+ maven_header_name: 'Job-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347577'
+ }
+ }
+ end
+
+ with_them do
+ let(:token) do
+ case authentication_token_type
+ when :personal_access_token
+ personal_access_token
+ when :ci_job_token
+ '${env.CI_JOB_TOKEN}'
+ when :project_deploy_token
+ project_deploy_token.token
+ end
+ end
+
+ before do
+ Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled)
+ end
+
+ it 'allows users to publish group level Maven packages duplicates', testcase: params[:testcase] do
+ create_duplicated_package
+
+ push_duplicated_package
+
+ show_latest_deploy_job
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+ end
+ end
+ end
+
+ def create_duplicated_package
+ settings_xml_with_pat = ERB.new(read_fixture('package_managers/maven', 'settings_with_pat.xml.erb')).result(binding)
+ package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding)
+
+ with_fixtures([
+ {
+ file_path: 'pom.xml',
+ content: package_pom_xml
+ },
+ {
+ file_path: 'settings.xml',
+ content: settings_xml_with_pat
+ }
+ ]) do |dir|
+ Service::DockerRun::Maven.new(dir).publish!
+ end
+
+ package_project.visit!
+
+ Page::Project::Menu.perform(&:click_packages_link)
+
+ Page::Project::Packages::Index.perform do |index|
+ expect(index).to have_package(package_name)
+ end
+ end
+
+ def push_duplicated_package
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ maven_upload_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_upload_package.yaml.erb')).result(binding)
+ package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding)
+ settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding)
+
+ commit.project = client_project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([
+ {
+ file_path: '.gitlab-ci.yml',
+ content: maven_upload_package_yaml
+ },
+ {
+ file_path: 'pom.xml',
+ content: package_pom_xml
+ },
+ {
+ file_path: 'settings.xml',
+ content: settings_xml
+ }
+ ])
+ end
+ end
+ end
+
+ def show_latest_deploy_job
+ client_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('deploy')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
new file mode 100644
index 00000000000..d79f65764d4
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_project_level_spec.rb
@@ -0,0 +1,219 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Package', :orchestrated, :packages, :object_storage do
+ describe 'Maven project level endpoint' do
+ let(:group_id) { 'com.gitlab.qa' }
+ let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
+ let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
+ let(:package_version) { '1.3.7' }
+ let(:package_type) { 'maven' }
+ let(:personal_access_token) { Runtime::Env.personal_access_token }
+
+ let(:package_project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = "#{package_type}_package_project"
+ project.initialize_with_readme = true
+ project.visibility = :private
+ end
+ end
+
+ let(:package) do
+ Resource::Package.init do |package|
+ package.name = package_name
+ package.project = package_project
+ end
+ end
+
+ let(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = "qa-runner-#{Time.now.to_i}"
+ runner.tags = ["runner-for-#{package_project.name}"]
+ runner.executor = :docker
+ runner.project = package_project
+ end
+ end
+
+ let(:gitlab_address_with_port) do
+ uri = URI.parse(Runtime::Scenario.gitlab_address)
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
+ end
+
+ let(:project_deploy_token) do
+ Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
+ deploy_token.name = 'package-deploy-token'
+ deploy_token.project = package_project
+ deploy_token.scopes = %w[
+ read_repository
+ read_package_registry
+ write_package_registry
+ ]
+ end
+ end
+
+ let(:gitlab_ci_file) do
+ {
+ file_path: '.gitlab-ci.yml',
+ content:
+ <<~YAML
+ deploy-and-install:
+ image: maven:3.6-jdk-11
+ script:
+ - 'mvn deploy -s settings.xml'
+ - 'mvn install -s settings.xml'
+ only:
+ - "#{package_project.default_branch}"
+ tags:
+ - "runner-for-#{package_project.name}"
+ YAML
+ }
+ end
+
+ let(:pom_file) do
+ {
+ file_path: 'pom.xml',
+ content: <<~XML
+ <project>
+ <groupId>#{group_id}</groupId>
+ <artifactId>#{artifact_id}</artifactId>
+ <version>#{package_version}</version>
+ <modelVersion>4.0.0</modelVersion>
+ <repositories>
+ <repository>
+ <id>#{package_project.name}</id>
+ <url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/-/packages/maven</url>
+ </repository>
+ </repositories>
+ <distributionManagement>
+ <repository>
+ <id>#{package_project.name}</id>
+ <url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url>
+ </repository>
+ <snapshotRepository>
+ <id>#{package_project.name}</id>
+ <url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url>
+ </snapshotRepository>
+ </distributionManagement>
+ </project>
+ XML
+ }
+ end
+
+ before do
+ Flow::Login.sign_in_unless_signed_in
+ runner
+ end
+
+ after do
+ runner.remove_via_api!
+ package.remove_via_api!
+ package_project.remove_via_api!
+ end
+
+ where do
+ {
+ 'using a personal access token' => {
+ authentication_token_type: :personal_access_token,
+ maven_header_name: 'Private-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354347'
+ },
+ 'using a project deploy token' => {
+ authentication_token_type: :project_deploy_token,
+ maven_header_name: 'Deploy-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354348'
+ },
+ 'using a ci job token' => {
+ authentication_token_type: :ci_job_token,
+ maven_header_name: 'Job-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354349'
+ }
+ }
+ end
+
+ with_them do
+ let(:token) do
+ case authentication_token_type
+ when :personal_access_token
+ personal_access_token
+ when :ci_job_token
+ '${env.CI_JOB_TOKEN}'
+ when :project_deploy_token
+ project_deploy_token.token
+ end
+ end
+
+ let(:settings_xml) do
+ {
+ 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>#{package_project.name}</id>
+ <configuration>
+ <httpHeaders>
+ <property>
+ <name>#{maven_header_name}</name>
+ <value>#{token}</value>
+ </property>
+ </httpHeaders>
+ </configuration>
+ </server>
+ </servers>
+ </settings>
+ XML
+ }
+ end
+
+ it 'pushes and pulls a maven package via maven', testcase: params[:testcase] do
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = package_project
+ commit.commit_message = 'Add .gitlab-ci.yml'
+ commit.add_files([
+ gitlab_ci_file,
+ pom_file,
+ settings_xml
+ ])
+ end
+ end
+
+ package_project.visit!
+
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('deploy')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+
+ job.click_element(:pipeline_path)
+ end
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('install')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+
+ 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, package_version)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb
deleted file mode 100644
index b4ebb9dd475..00000000000
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb
+++ /dev/null
@@ -1,233 +0,0 @@
-# frozen_string_literal: true
-
-module QA
- RSpec.describe 'Package', :orchestrated, :packages, :object_storage do
- describe 'Maven Repository' do
- using RSpec::Parameterized::TableSyntax
- include Runtime::Fixtures
- include_context 'packages registry qa scenario'
-
- let(:group_id) { 'com.gitlab.qa' }
- let(:artifact_id) { "maven-#{SecureRandom.hex(8)}" }
- let(:package_name) { "#{group_id}/#{artifact_id}".tr('.', '/') }
- let(:package_version) { '1.3.7' }
- let(:package_type) { 'maven' }
-
- where(:authentication_token_type, :maven_header_name) do
- :personal_access_token | 'Private-Token'
- :ci_job_token | 'Job-Token'
- :project_deploy_token | 'Deploy-Token'
- end
-
- with_them do
- let(:token) do
- case authentication_token_type
- when :personal_access_token
- personal_access_token
- when :ci_job_token
- '${env.CI_JOB_TOKEN}'
- when :project_deploy_token
- project_deploy_token.token
- end
- end
-
- it "pushes and pulls a maven package via maven using #{params[:authentication_token_type]}" do
- Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- maven_upload_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_upload_package.yaml.erb')).result(binding)
- package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding)
- settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding)
-
- commit.project = package_project
- commit.commit_message = 'Add files'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: maven_upload_package_yaml
- },
- {
- file_path: 'pom.xml',
- content: package_pom_xml
- },
- {
- file_path: 'settings.xml',
- content: settings_xml
- }
- ])
- end
- end
-
- package_project.visit!
-
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('deploy')
- end
-
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
-
- 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, package_version)
- end
-
- Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- maven_install_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_install_package.yaml.erb')).result(binding)
- client_pom_xml = ERB.new(read_fixture('package_managers/maven', 'client_pom.xml.erb')).result(binding)
- settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding)
-
- commit.project = client_project
- commit.commit_message = 'Add files'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: maven_install_package_yaml
- },
- {
- file_path: 'pom.xml',
- content: client_pom_xml
- },
- {
- file_path: 'settings.xml',
- content: settings_xml
- }
- ])
- end
- end
-
- client_project.visit!
-
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('install')
- end
-
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
- end
-
- context 'duplication setting' do
- before do
- package_project.group.visit!
-
- Page::Group::Menu.perform(&:go_to_package_settings)
- end
-
- context 'when disabled' do
- before do
- Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled)
- end
-
- it "prevents users from publishing group level Maven packages duplicates using #{params[:authentication_token_type]}" do
- create_duplicated_package
-
- push_duplicated_package
-
- client_project.visit!
-
- show_latest_deploy_job
-
- Page::Project::Job::Show.perform do |job|
- expect(job).not_to be_successful(timeout: 800)
- end
- end
- end
-
- context 'when enabled' do
- before do
- Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled)
- end
-
- it "allows users to publish group level Maven packages duplicates using #{params[:authentication_token_type]}" do
- create_duplicated_package
-
- push_duplicated_package
-
- show_latest_deploy_job
-
- Page::Project::Job::Show.perform do |job|
- expect(job).to be_successful(timeout: 800)
- end
- end
- end
-
- def create_duplicated_package
- settings_xml_with_pat = ERB.new(read_fixture('package_managers/maven', 'settings_with_pat.xml.erb')).result(binding)
- package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding)
-
- with_fixtures([
- {
- file_path: 'pom.xml',
- content: package_pom_xml
- },
- {
- file_path: 'settings.xml',
- content: settings_xml_with_pat
- }
- ]) do |dir|
- Service::DockerRun::Maven.new(dir).publish!
- end
-
- package_project.visit!
-
- Page::Project::Menu.perform(&:click_packages_link)
-
- Page::Project::Packages::Index.perform do |index|
- expect(index).to have_package(package_name)
- end
- end
-
- def push_duplicated_package
- Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
- Resource::Repository::Commit.fabricate_via_api! do |commit|
- maven_upload_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_upload_package.yaml.erb')).result(binding)
- package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding)
- settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding)
-
- commit.project = client_project
- commit.commit_message = 'Add .gitlab-ci.yml'
- commit.add_files([
- {
- file_path: '.gitlab-ci.yml',
- content: maven_upload_package_yaml
- },
- {
- file_path: 'pom.xml',
- content: package_pom_xml
- },
- {
- file_path: 'settings.xml',
- content: settings_xml
- }
- ])
- end
- end
- end
-
- def show_latest_deploy_job
- client_project.visit!
-
- Flow::Pipeline.visit_latest_pipeline
-
- Page::Project::Pipeline::Show.perform do |pipeline|
- pipeline.click_job('deploy')
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb
index 24f83bc19fb..b0a6555a16b 100644
--- a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_group_level_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Package', :orchestrated, :packages, :object_storage do
- describe 'NuGet Repository' do
+ describe 'NuGet group level endpoint' do
using RSpec::Parameterized::TableSyntax
include Runtime::Fixtures
diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
new file mode 100644
index 00000000000..4cac055634e
--- /dev/null
+++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget/nuget_project_level_spec.rb
@@ -0,0 +1,164 @@
+# frozen_string_literal: true
+
+module QA
+ RSpec.describe 'Package', :orchestrated, :packages, :object_storage do
+ describe 'NuGet project level endpoint' do
+ let(:project) do
+ Resource::Project.fabricate_via_api! do |project|
+ project.name = 'nuget-package-project'
+ project.template_name = 'dotnetcore'
+ project.visibility = :private
+ end
+ end
+
+ let(:personal_access_token) do
+ unless Page::Main::Menu.perform(&:signed_in?)
+ Flow::Login.sign_in
+ end
+
+ Resource::PersonalAccessToken.fabricate!
+ end
+
+ let(:project_deploy_token) do
+ Resource::ProjectDeployToken.fabricate_via_api! do |deploy_token|
+ deploy_token.name = 'package-deploy-token'
+ deploy_token.project = project
+ deploy_token.scopes = %w[
+ read_repository
+ read_package_registry
+ write_package_registry
+ ]
+ end
+ end
+
+ let(:package) do
+ Resource::Package.init do |package|
+ package.name = "dotnetcore-#{SecureRandom.hex(8)}"
+ package.project = project
+ end
+ end
+
+ let!(:runner) do
+ Resource::Runner.fabricate! do |runner|
+ runner.name = "qa-runner-#{Time.now.to_i}"
+ runner.tags = ["runner-for-#{project.name}"]
+ runner.executor = :docker
+ runner.project = project
+ end
+ end
+
+ after do
+ runner.remove_via_api!
+ package.remove_via_api!
+ project.remove_via_api!
+ end
+
+ where do
+ {
+ 'using a personal access token' => {
+ authentication_token_type: :personal_access_token,
+ maven_header_name: 'Private-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354351'
+ },
+ 'using a project deploy token' => {
+ authentication_token_type: :project_deploy_token,
+ maven_header_name: 'Deploy-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354352'
+ },
+ 'using a ci job token' => {
+ authentication_token_type: :ci_job_token,
+ maven_header_name: 'Job-Token',
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/354353'
+ }
+ }
+ end
+
+ with_them do
+ let(:auth_token_password) do
+ case authentication_token_type
+ when :personal_access_token
+ "\"#{personal_access_token.token}\""
+ when :ci_job_token
+ '${CI_JOB_TOKEN}'
+ when :project_deploy_token
+ "\"#{project_deploy_token.token}\""
+ end
+ end
+
+ let(:auth_token_username) do
+ case authentication_token_type
+ when :personal_access_token
+ "\"#{personal_access_token.user.username}\""
+ when :ci_job_token
+ 'gitlab-ci-token'
+ when :project_deploy_token
+ "\"#{project_deploy_token.username}\""
+ end
+ end
+
+ it 'publishes a nuget package and installs', testcase: params[:testcase] do
+ Flow::Login.sign_in
+
+ Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do
+ Resource::Repository::Commit.fabricate_via_api! do |commit|
+ commit.project = project
+ commit.commit_message = 'Add files'
+ commit.update_files(
+ [
+ {
+ file_path: '.gitlab-ci.yml',
+ content: <<~YAML
+ deploy-and-install:
+ image: mcr.microsoft.com/dotnet/sdk:5.0
+ script:
+ - dotnet restore -p:Configuration=Release
+ - dotnet build -c Release
+ - dotnet pack -c Release -p:PackageID=#{package.name}
+ - dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username #{auth_token_username} --password #{auth_token_password} --store-password-in-clear-text
+ - dotnet nuget push "bin/Release/*.nupkg" --source gitlab
+ - "dotnet add dotnetcore.csproj package #{package.name} --version 1.0.0"
+ rules:
+ - if: '$CI_COMMIT_BRANCH == "#{project.default_branch}"'
+ tags:
+ - "runner-for-#{project.name}"
+ YAML
+ },
+ {
+ file_path: 'dotnetcore.csproj',
+ content: <<~EOF
+ <Project Sdk="Microsoft.NET.Sdk">
+
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net5.0</TargetFramework>
+ </PropertyGroup>
+
+ </Project>
+ EOF
+ }
+ ]
+ )
+ end
+ end
+
+ project.visit!
+ Flow::Pipeline.visit_latest_pipeline
+
+ Page::Project::Pipeline::Show.perform do |pipeline|
+ pipeline.click_job('deploy-and-install')
+ end
+
+ Page::Project::Job::Show.perform do |job|
+ expect(job).to be_successful(timeout: 800)
+ end
+
+ Page::Project::Menu.perform(&:click_packages_link)
+
+ Page::Project::Packages::Index.perform do |index|
+ expect(index).to have_package(package.name)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
index 260c812420c..1661fec03be 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb
@@ -2,7 +2,7 @@
module QA
RSpec.describe 'Release' do
- describe 'Deploy key creation' do
+ describe 'Deploy key creation', :skip_fips_env do
it 'user adds a deploy key', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348023' do
Flow::Login.sign_in
@@ -10,7 +10,7 @@ module QA
deploy_key_title = 'deploy key title'
deploy_key_value = key.public_key
- deploy_key = Resource::DeployKey.fabricate! do |resource|
+ deploy_key = Resource::DeployKey.fabricate_via_browser_ui! do |resource|
resource.title = deploy_key_title
resource.key = deploy_key_value
end
diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
index c86f75e0b16..ff8dc686991 100644
--- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
+++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb
@@ -4,7 +4,7 @@ require 'digest/sha1'
module QA
RSpec.describe 'Release', :runner do
- describe 'Git clone using a deploy key' do
+ describe 'Git clone using a deploy key', :skip_fips_env do
let(:runner_name) { "qa-runner-#{SecureRandom.hex(4)}" }
let(:repository_location) { project.repository_ssh_location }
diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb
index 718dc9860fb..980c6da2576 100644
--- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb
+++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb
@@ -56,7 +56,7 @@ module QA
end
Page::Project::Job::Show.perform do |show|
- expect(show).to have_passed(timeout: 360)
+ expect(show).to have_passed(timeout: 800)
end
end
end
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 70321dcafe4..e0cd5a52bfb 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
@@ -10,6 +10,7 @@ module QA
end
before do
+ set_kube_ingress_base_domain(project)
disable_optional_jobs(project)
end
@@ -73,6 +74,15 @@ module QA
private
+ def set_kube_ingress_base_domain(project)
+ Resource::CiVariable.fabricate_via_api! do |resource|
+ resource.project = project
+ resource.key = 'KUBE_INGRESS_BASE_DOMAIN'
+ resource.value = 'example.com'
+ resource.masked = false
+ end
+ end
+
def disable_optional_jobs(project)
%w[
CODE_QUALITY_DISABLED LICENSE_MANAGEMENT_DISABLED
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index a861c13a44c..2b9adf0e870 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -7,6 +7,7 @@ module QA
module Specs
class Runner < Scenario::Template
attr_accessor :tty, :tags, :options
+
RegexMismatchError = Class.new(StandardError)
DEFAULT_TEST_PATH_ARGS = ['--', File.expand_path('./features', __dir__)].freeze
@@ -40,9 +41,8 @@ module QA
end
tags_for_rspec.push(%w[--tag ~geo]) unless QA::Runtime::Env.geo_environment?
-
tags_for_rspec.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled?
-
+ tags_for_rspec.push(%w[--tag ~smoke --tag ~reliable]) if QA::Runtime::Env.skip_smoke_reliable?
tags_for_rspec.push(%w[--tag ~skip_live_env]) if QA::Specs::Helpers::ContextSelector.dot_com?
QA::Runtime::Env.supported_features.each_key do |key|
diff --git a/qa/qa/support/dates.rb b/qa/qa/support/dates.rb
index 3d1f146730b..9e791bc11c5 100644
--- a/qa/qa/support/dates.rb
+++ b/qa/qa/support/dates.rb
@@ -11,6 +11,10 @@ module QA
current_date.next_month.strftime("%Y/%m/%d")
end
+ def thirteen_days_from_now_yyyy_mm_dd
+ (current_date + 13).strftime("%Y/%m/%d")
+ end
+
def format_date(date)
new_date = DateTime.strptime(date, "%Y/%m/%d")
new_date.strftime("%b %-d, %Y")
diff --git a/qa/qa/support/formatters/allure_metadata_formatter.rb b/qa/qa/support/formatters/allure_metadata_formatter.rb
index da35ffde1ae..2ed4ee2066a 100644
--- a/qa/qa/support/formatters/allure_metadata_formatter.rb
+++ b/qa/qa/support/formatters/allure_metadata_formatter.rb
@@ -4,20 +4,41 @@ module QA
module Support
module Formatters
class AllureMetadataFormatter < ::RSpec::Core::Formatters::BaseFormatter
+ include Support::InfluxdbTools
+
::RSpec::Core::Formatters.register(
self,
- :example_started
+ :start,
+ :example_finished
)
- # Starts example
+ # Starts test run
+ # Fetch flakiness data in mr pipelines to help identify unrelated flaky failures
+ #
+ # @param [RSpec::Core::Notifications::StartNotification] _start_notification
+ # @return [void]
+ def start(_start_notification)
+ return unless merge_request_iid # on main runs allure native history has pass rate already
+
+ save_failures
+ log(:debug, "Fetched #{failures.length} flaky testcases!")
+ rescue StandardError => e
+ log(:error, "Failed to fetch flaky spec data for report: #{e}")
+ @failures = {}
+ end
+
+ # Finished example
+ # Add additional metadata to report
+ #
# @param [RSpec::Core::Notifications::ExampleNotification] example_notification
# @return [void]
- def example_started(example_notification)
+ def example_finished(example_notification)
example = example_notification.example
add_quarantine_issue_link(example)
add_failure_issues_link(example)
add_ci_job_link(example)
+ set_flaky_status(example)
end
private
@@ -55,6 +76,66 @@ module QA
example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url)
end
+
+ # Mark test as flaky
+ #
+ # @param [RSpec::Core::Example] example
+ # @return [void]
+ def set_flaky_status(example)
+ return unless merge_request_iid
+ return unless example.execution_result.status == :failed && failures.key?(example.metadata[:testcase])
+
+ example.set_flaky
+ example.parameter("pass_rate", "#{failures[example.metadata[:testcase]].round(1)}%")
+ log(:debug, "Setting spec as flaky due to present failures in last 14 days!")
+ end
+
+ # Failed spec testcases
+ #
+ # @return [Array]
+ def failures
+ @failures ||= influx_data.lazy.each_with_object({}) do |data, result|
+ # TODO: replace with mr_iid once stats are populated
+ records = data.records.reject { |r| r.values["_value"] == env("CI_PIPELINE_ID") }
+
+ runs = records.count
+ failed = records.count { |r| r.values["status"] == "failed" }
+ pass_rate = 100 - ((failed.to_f / runs.to_f) * 100)
+
+ # Consider spec with a pass rate less than 98% as flaky
+ result[records.last.values["testcase"]] = pass_rate if pass_rate < 98
+ end.compact
+ end
+
+ alias_method :save_failures, :failures
+
+ # Records of previous failures for runs of same type
+ #
+ # @return [Array]
+ def influx_data
+ return [] unless run_type
+
+ query_api.query(query: <<~QUERY).values
+ from(bucket: "#{Support::InfluxdbTools::INFLUX_TEST_METRICS_BUCKET}")
+ |> range(start: -14d)
+ |> filter(fn: (r) => r._measurement == "test-stats")
+ |> filter(fn: (r) => r.run_type == "#{run_type}" and
+ r.status != "pending" and
+ r.quarantined == "false" and
+ r._field == "pipeline_id"
+ )
+ |> group(columns: ["testcase"])
+ QUERY
+ end
+
+ # Print log message
+ #
+ # @param [Symbol] level
+ # @param [String] message
+ # @return [void]
+ def log(level, message)
+ QA::Runtime::Logger.public_send(level, "[Allure]: #{message}")
+ end
end
end
end
diff --git a/qa/qa/support/formatters/test_stats_formatter.rb b/qa/qa/support/formatters/test_stats_formatter.rb
index 430294b0bb6..16fc0a50b1b 100644
--- a/qa/qa/support/formatters/test_stats_formatter.rb
+++ b/qa/qa/support/formatters/test_stats_formatter.rb
@@ -4,6 +4,8 @@ module QA
module Support
module Formatters
class TestStatsFormatter < RSpec::Core::Formatters::BaseFormatter
+ include Support::InfluxdbTools
+
RSpec::Core::Formatters.register(self, :stop)
# Finish test execution
@@ -11,9 +13,6 @@ module QA
# @param [RSpec::Core::Notifications::ExamplesNotification] notification
# @return [void]
def stop(notification)
- return log(:warn, 'Missing QA_INFLUXDB_URL, skipping metrics export!') unless influxdb_url
- return log(:warn, 'Missing QA_INFLUXDB_TOKEN, skipping metrics export!') unless influxdb_token
-
push_test_stats(notification.examples)
push_fabrication_stats
end
@@ -27,7 +26,7 @@ module QA
def push_test_stats(examples)
data = examples.map { |example| test_stats(example) }.compact
- influx_client.write(data: data)
+ write_api.write(data: data)
log(:debug, "Pushed #{data.length} test execution entries to influxdb")
rescue StandardError => e
log(:error, "Failed to push test execution stats to influxdb, error: #{e}")
@@ -42,7 +41,7 @@ module QA
end
return if data.empty?
- influx_client.write(data: data)
+ write_api.write(data: data)
log(:debug, "Pushed #{data.length} resource fabrication entries to influxdb")
rescue StandardError => e
log(:error, "Failed to push fabrication stats to influxdb, error: #{e}")
@@ -66,11 +65,11 @@ module QA
file_path: file_path,
status: example.execution_result.status,
reliable: example.metadata.key?(:reliable).to_s,
- quarantined: example.metadata.key?(:quarantine).to_s,
+ quarantined: quarantined(example.metadata),
retried: ((example.metadata[:retry_attempts] || 0) > 0).to_s,
job_name: job_name,
merge_request: merge_request,
- run_type: env('QA_RUN_TYPE') || run_type,
+ run_type: run_type,
stage: devops_stage(file_path),
testcase: example.metadata[:testcase]
},
@@ -83,7 +82,8 @@ module QA
retry_attempts: example.metadata[:retry_attempts] || 0,
job_url: QA::Runtime::Env.ci_job_url,
pipeline_url: env('CI_PIPELINE_URL'),
- pipeline_id: env('CI_PIPELINE_ID')
+ pipeline_id: env('CI_PIPELINE_ID'),
+ merge_request_iid: merge_request_iid
}
}
rescue StandardError => e
@@ -119,13 +119,6 @@ module QA
}
end
- # Project name
- #
- # @return [String]
- def project_name
- @project_name ||= QA::Runtime::Env.ci_project_name
- end
-
# Base ci job name
#
# @return [String]
@@ -148,26 +141,18 @@ module QA
#
# @return [String]
def merge_request
- @merge_request ||= (!!env('CI_MERGE_REQUEST_IID') || !!env('TOP_UPSTREAM_MERGE_REQUEST_IID')).to_s
+ (!!merge_request_iid).to_s
end
- # Test run type from staging (`gstg`, `gstg-cny`, `gstg-ref`), canary, preprod or production env
+ # Is spec quarantined
#
- # @return [String, nil]
- def run_type
- return unless %w[staging staging-canary staging-ref canary preprod production].include?(project_name)
-
- @run_type ||= begin
- test_subset = if env('NO_ADMIN') == 'true'
- 'sanity-no-admin'
- elsif env('SMOKE_ONLY') == 'true'
- 'sanity'
- else
- 'full'
- end
-
- "#{project_name}-#{test_subset}"
- end
+ # @param [Hash] metadata
+ # @return [String]
+ def quarantined(metadata)
+ return "false" unless metadata.key?(:quarantine)
+ return "true" unless metadata[:quarantine].is_a?(Hash)
+
+ (!Specs::Helpers::Quarantine.quarantined_different_context?(metadata[:quarantine])).to_s
end
# Print log message
@@ -179,16 +164,6 @@ module QA
QA::Runtime::Logger.public_send(level, "[influxdb exporter]: #{message}")
end
- # Return non empty environment variable value
- #
- # @param [String] name
- # @return [String, nil]
- def env(name)
- return unless ENV[name] && !ENV[name].empty?
-
- ENV[name]
- end
-
# Get spec devops stage
#
# @param [String] location
@@ -196,33 +171,6 @@ module QA
def devops_stage(file_path)
file_path.match(%r{\d{1,2}_(\w+)/})&.captures&.first
end
-
- # InfluxDb client
- #
- # @return [InfluxDB2::WriteApi]
- def influx_client
- @influx_client ||= InfluxDB2::Client.new(
- influxdb_url,
- influxdb_token,
- bucket: 'e2e-test-stats',
- org: 'gitlab-qa',
- precision: InfluxDB2::WritePrecision::NANOSECOND
- ).create_write_api
- end
-
- # InfluxDb instance url
- #
- # @return [String]
- def influxdb_url
- @influxdb_url ||= env('QA_INFLUXDB_URL')
- end
-
- # Influxdb token
- #
- # @return [String]
- def influxdb_token
- @influxdb_token ||= env('QA_INFLUXDB_TOKEN')
- end
end
end
end
diff --git a/qa/qa/support/influxdb_tools.rb b/qa/qa/support/influxdb_tools.rb
new file mode 100644
index 00000000000..e53b843ca87
--- /dev/null
+++ b/qa/qa/support/influxdb_tools.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+require "active_support/core_ext/module/delegation"
+
+module QA
+ module Support
+ # Common tools for use with influxdb metrics setup
+ #
+ module InfluxdbTools
+ INFLUX_TEST_METRICS_BUCKET = "e2e-test-stats"
+ LIVE_ENVS = %w[staging staging-canary staging-ref canary preprod production].freeze
+
+ private
+
+ delegate :ci_project_name, to: "QA::Runtime::Env"
+
+ # Query client
+ #
+ # @return [QueryApi]
+ def query_api
+ @query_api ||= influx_client.create_query_api
+ end
+
+ # Write client
+ #
+ # @return [WriteApi]
+ def write_api
+ @write_api ||= influx_client.create_write_api
+ end
+
+ # InfluxDb client
+ #
+ # @return [InfluxDB2::Client]
+ def influx_client
+ @influx_client ||= InfluxDB2::Client.new(
+ ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL env variable"),
+ ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN env variable"),
+ bucket: INFLUX_TEST_METRICS_BUCKET,
+ org: "gitlab-qa",
+ precision: InfluxDB2::WritePrecision::NANOSECOND
+ )
+ end
+
+ # Test run type
+ # Automatically infer for staging (`gstg`, `gstg-cny`, `gstg-ref`), canary, preprod or production env
+ #
+ # @return [String, nil]
+ def run_type
+ @run_type ||= begin
+ return env('QA_RUN_TYPE') if env('QA_RUN_TYPE')
+ return unless LIVE_ENVS.include?(ci_project_name)
+
+ test_subset = if env('NO_ADMIN') == 'true'
+ 'sanity-no-admin'
+ elsif env('SMOKE_ONLY') == 'true'
+ 'sanity'
+ else
+ 'full'
+ end
+
+ "#{ci_project_name}-#{test_subset}"
+ end
+ end
+
+ # Merge request iid
+ #
+ # @return [String]
+ def merge_request_iid
+ env('CI_MERGE_REQUEST_IID') || env('TOP_UPSTREAM_MERGE_REQUEST_IID')
+ end
+
+ # Return non empty environment variable value
+ #
+ # @param [String] name
+ # @return [String, nil]
+ def env(name)
+ return unless ENV[name] && !ENV[name].empty?
+
+ ENV[name]
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/loglinking.rb b/qa/qa/support/loglinking.rb
new file mode 100644
index 00000000000..89519e9537c
--- /dev/null
+++ b/qa/qa/support/loglinking.rb
@@ -0,0 +1,86 @@
+# frozen_string_literal: true
+module QA
+ module Support
+ module Loglinking
+ # Static address variables declared for mapping environment to logging URLs
+ STAGING_ADDRESS = 'https://staging.gitlab.com'
+ STAGING_REF_ADDRESS = 'https://staging-ref.gitlab.com'
+ PRODUCTION_ADDRESS = 'https://www.gitlab.com'
+ PRE_PROD_ADDRESS = 'https://pre.gitlab.com'
+ SENTRY_ENVIRONMENTS = {
+ staging: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
+ staging_canary: 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
+ staging_ref: 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
+ pre: 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
+ canary: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
+ production: 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny'
+ }.freeze
+ KIBANA_ENVIRONMENTS = {
+ staging: 'https://nonprod-log.gitlab.net/',
+ staging_canary: 'https://nonprod-log.gitlab.net/',
+ canary: 'https://log.gprd.gitlab.net/',
+ production: 'https://log.gprd.gitlab.net/'
+ }.freeze
+
+ def self.failure_metadata(correlation_id)
+ return if correlation_id.blank?
+
+ sentry_uri = sentry_url
+ kibana_uri = kibana_url
+
+ errors = ["Correlation Id: #{correlation_id}"]
+ errors << "Sentry Url: #{sentry_uri}&query=correlation_id%3A%22#{correlation_id}%22" if sentry_uri
+ errors << "Kibana Url: #{kibana_uri}app/discover#/?_a=(query:(language:kuery,query:'json.correlation_id%20:%20#{correlation_id}'))" if kibana_uri
+
+ errors.join("\n")
+ end
+
+ def self.sentry_url
+ return unless logging_environment?
+
+ SENTRY_ENVIRONMENTS[logging_environment]
+ end
+
+ def self.kibana_url
+ return unless logging_environment?
+
+ KIBANA_ENVIRONMENTS[logging_environment]
+ end
+
+ def self.logging_environment
+ address = QA::Runtime::Scenario.attributes[:gitlab_address]
+ return if address.nil?
+
+ case address
+ when STAGING_ADDRESS
+ canary? ? :staging_canary : :staging
+ when STAGING_REF_ADDRESS
+ :staging_ref
+ when PRODUCTION_ADDRESS
+ canary? ? :canary : :production
+ when PRE_PROD_ADDRESS
+ :pre
+ else
+ nil
+ end
+ end
+
+ def self.logging_environment?
+ !logging_environment.nil?
+ end
+
+ def self.cookies
+ browser_cookies = Capybara.current_session.driver.browser.manage.all_cookies
+ # rubocop:disable Rails/IndexBy
+ browser_cookies.each_with_object({}) do |cookie, memo|
+ memo[cookie[:name]] = cookie
+ end
+ # rubocop:enable Rails/IndexBy
+ end
+
+ def self.canary?
+ cookies.dig('gitlab_canary', :value) == 'true'
+ end
+ end
+ end
+end
diff --git a/qa/qa/support/page_error_checker.rb b/qa/qa/support/page_error_checker.rb
index 5d16245b4cd..ede9b49bda6 100644
--- a/qa/qa/support/page_error_checker.rb
+++ b/qa/qa/support/page_error_checker.rb
@@ -5,14 +5,28 @@ module QA
class PageErrorChecker
class << self
def report!(page, error_code)
+ request_id_string = ''
+ if error_code == 500
+ request_id = parse_five_c_page_request_id(page)
+ if request_id
+ request_id_string = "\n\n" + Loglinking.failure_metadata(request_id)
+ end
+ end
+
report = if QA::Runtime::Env.browser == :chrome
return_chrome_errors(page, error_code)
else
status_code_report(error_code)
end
- raise "#{report}\n\n"\
- "Path: #{page.current_path}"
+ raise "Error Code #{error_code}\n\n"\
+ "#{report}\n\n"\
+ "Path: #{page.current_path}"\
+ "#{request_id_string}"
+ end
+
+ def parse_five_c_page_request_id(page)
+ Nokogiri::HTML.parse(page.html).xpath("/html/body/div/p[1]/code").children.first
end
def return_chrome_errors(page, error_code)
diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb
index 1b9aa809051..6956987de05 100644
--- a/qa/qa/support/repeater.rb
+++ b/qa/qa/support/repeater.rb
@@ -1,6 +1,5 @@
# frozen_string_literal: true
require 'active_support/inflector'
-require 'rainbow/refinement'
module QA
module Support
@@ -40,7 +39,9 @@ module QA
QA::Runtime::Logger.debug(msg.join(' '))
end
- QA::Runtime::Logger.debug("Attempt number #{attempts + 1}".bg(:yellow).black) if log && max_attempts && attempts > 0
+ if log && max_attempts && attempts > 0
+ QA::Runtime::Logger.debug("Attempt number #{attempts + 1}".bg(:yellow).black)
+ end
result = yield
if result
diff --git a/qa/qa/tools/delete_test_resources.rb b/qa/qa/tools/delete_test_resources.rb
deleted file mode 100644
index 6f63c56f736..00000000000
--- a/qa/qa/tools/delete_test_resources.rb
+++ /dev/null
@@ -1,100 +0,0 @@
-# frozen_string_literal: true
-
-# This script reads from test-resources JSON file to collect data about resources to delete
-# Filter out resources that cannot be deleted
-# Then deletes all deletable resources that E2E tests created
-#
-# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS
-# When in CI also requires: QA_TEST_RESOURCES_FILE_PATTERN
-# Run `rake delete_test_resources[<file_pattern>]`
-
-module QA
- module Tools
- class DeleteTestResources
- include Support::API
-
- IGNORED_RESOURCES = [
- 'QA::Resource::PersonalAccessToken',
- 'QA::Resource::CiVariable',
- 'QA::Resource::Repository::Commit',
- 'QA::EE::Resource::GroupIteration',
- 'QA::EE::Resource::Settings::Elasticsearch'
- ].freeze
-
- def initialize(file_pattern = Runtime::Env.test_resources_created_filepath)
- raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
- raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN']
-
- @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
- @file_pattern = file_pattern
- end
-
- def run
- failures = files.flat_map do |file|
- resources = read_file(file)
- filtered_resources = filter_resources(resources)
- delete_resources(filtered_resources)
- end
-
- return puts "\nDone" if failures.empty?
-
- puts "\nFailed to delete #{failures.size} resources:\n"
- puts failures
- end
-
- private
-
- def files
- puts "Gathering JSON files...\n"
- files = Dir.glob(@file_pattern)
- abort("There is no file with this pattern #{@file_pattern}") if files.empty?
-
- files.reject { |file| File.zero?(file) }
-
- files
- end
-
- def read_file(file)
- JSON.parse(File.read(file))
- end
-
- def filter_resources(resources)
- puts "Filtering resources - Only keep deletable resources...\n"
-
- transformed_values = resources.transform_values! do |v|
- v.reject do |attributes|
- attributes['info'] == "with full_path 'gitlab-qa-sandbox-group'" ||
- attributes['http_method'] == 'get' && !attributes['info']&.include?("with username 'qa-") ||
- attributes['api_path'] == 'Cannot find resource API path'
- end
- end
-
- transformed_values.reject! { |k, v| v.empty? || IGNORED_RESOURCES.include?(k) }
- end
-
- def delete_resources(resources)
- resources.each_with_object([]) do |(key, value), failures|
- value.each do |resource|
- next if resource_not_found?(resource['api_path'])
-
- msg = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}"
- puts "\nDeleting #{msg}..."
- delete_response = delete(Runtime::API::Request.new(@api_client, resource['api_path']).url)
-
- if delete_response.code == 202 || delete_response.code == 204
- print "\e[32m.\e[0m"
- else
- print "\e[31mF\e[0m"
- failures << msg
- end
- end
- end
- end
-
- def resource_not_found?(api_path)
- # if api path contains param "?hard_delete=<boolean>", remove it
- get(Runtime::API::Request.new(@api_client, api_path.split('?').first).url).code.eql? 404
- end
- end
- end
-end
diff --git a/qa/qa/tools/delete_test_users.rb b/qa/qa/tools/delete_test_users.rb
new file mode 100644
index 00000000000..30d3a82fb1b
--- /dev/null
+++ b/qa/qa/tools/delete_test_users.rb
@@ -0,0 +1,87 @@
+# frozen_string_literal: true
+
+# This script deletes users with a username starting with "qa-user-"
+# - Specify `delete_before` to delete only keys that were created before the given date (default: yesterday)
+# - If `dry_run` is true the script will list the users to be deleted by username, but it won't delete them
+# - Specify `exclude_users` as a comma-separated list of usernames to not delete.
+#
+# Required environment variables: GITLAB_QA_ADMIN_ACCESS_TOKEN and GITLAB_ADDRESS
+# - GITLAB_QA_ADMIN_ACCESS_TOKEN must have admin API access
+
+module QA
+ module Tools
+ class DeleteTestUsers
+ include Support::API
+
+ ITEMS_PER_PAGE = '100'
+ EXCLUDE_USERS = %w[qa-user-abc123].freeze
+ FALSY_VALUES = %w[false no 0].freeze
+
+ def initialize(delete_before: (Date.today - 1).to_s, dry_run: 'false', exclude_users: nil)
+ raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS']
+ raise ArgumentError, "Please provide GITLAB_QA_ADMIN_ACCESS_TOKEN" unless ENV['GITLAB_QA_ADMIN_ACCESS_TOKEN']
+
+ @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ADMIN_ACCESS_TOKEN'])
+ @dry_run = !FALSY_VALUES.include?(dry_run.to_s.downcase)
+ @delete_before = Date.parse(delete_before)
+ @page_no = '1'
+ @exclude_users = Array(exclude_users.to_s.split(',')) + EXCLUDE_USERS
+ end
+
+ def run
+ puts "Deleting users with a username starting with 'qa-user-' or 'test-user-' created before #{@delete_before}..."
+
+ while page_no.present?
+ users = fetch_test_users
+
+ delete_test_users(users) if users.present?
+ end
+
+ puts "\nDone"
+ end
+
+ private
+
+ attr_reader :dry_run, :page_no
+ alias_method :dry_run?, :dry_run
+
+ def fetch_test_users
+ puts "Fetching QA test users from page #{page_no}..."
+
+ response = get Runtime::API::Request.new(@api_client, "/users", page: page_no, per_page: ITEMS_PER_PAGE).url
+
+ # When we reach the last page, the x-next-page header is a blank string
+ @page_no = response.headers[:x_next_page].to_s
+
+ if @page_no.to_i > 1000
+ puts "Finishing early to avoid timing out the CI job"
+ exit
+ end
+
+ JSON.parse(response.body).select do |user|
+ user['username'].start_with?('qa-user-', 'test-user-') \
+ && (user['name'] == 'QA Tests' || user['name'].start_with?('QA User')) \
+ && !@exclude_users.include?(user['username']) \
+ && Date.parse(user.fetch('created_at', Date.today.to_s)) < @delete_before
+ end
+ end
+
+ def delete_test_users(users)
+ usernames = users.map { |user| user['username'] }.join(', ')
+ if dry_run?
+ puts "Dry run: found users with usernames #{usernames}"
+
+ return
+ end
+
+ puts "Deleting #{users.length} users with usernames #{usernames}..."
+ users.each do |user|
+ delete_response = delete Runtime::API::Request.new(@api_client, "/users/#{user['id']}", hard_delete: 'true').url
+ dot_or_f = delete_response.code == 204 ? "\e[32m.\e[0m" : "\e[31mF\e[0m"
+ print dot_or_f
+ end
+ print "\n"
+ end
+ end
+ end
+end
diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb
index 27e54c2d8bf..96e5690ce30 100644
--- a/qa/qa/tools/reliable_report.rb
+++ b/qa/qa/tools/reliable_report.rb
@@ -8,6 +8,7 @@ require "colorize"
module QA
module Tools
class ReliableReport
+ include Support::InfluxdbTools
include Support::API
# Project for report creation: https://gitlab.com/gitlab-org/gitlab
@@ -15,10 +16,7 @@ module QA
def initialize(range)
@range = range.to_i
- @influxdb_bucket = "e2e-test-stats"
@slack_channel = "#quality-reports"
- @influxdb_url = ENV["QA_INFLUXDB_URL"] || raise("Missing QA_INFLUXDB_URL env variable")
- @influxdb_token = ENV["QA_INFLUXDB_TOKEN"] || raise("Missing QA_INFLUXDB_TOKEN env variable")
end
# Run reliable reporter
@@ -58,7 +56,7 @@ module QA
{
title: "Reliable e2e test report",
description: report_issue_body,
- labels: "Quality,test,type::maintenance,reliable test report,automation:devops-mapping-disable"
+ labels: "Quality,test,type::maintenance,reliable test report,automation:ml"
},
headers: { "PRIVATE-TOKEN" => gitlab_access_token }
)
@@ -91,7 +89,7 @@ module QA
private
- attr_reader :range, :influxdb_bucket, :slack_channel, :influxdb_url, :influxdb_token
+ attr_reader :range, :slack_channel
# Markdown formatted report issue body
#
@@ -304,7 +302,7 @@ module QA
# @return [String]
def query(reliable)
<<~QUERY
- from(bucket: "#{influxdb_bucket}")
+ from(bucket: "#{Support::InfluxdbTools::INFLUX_TEST_METRICS_BUCKET}")
|> range(start: -#{range}d)
|> filter(fn: (r) => r._measurement == "test-stats")
|> filter(fn: (r) => r.run_type == "staging-full" or
@@ -325,26 +323,6 @@ module QA
QUERY
end
- # Query client
- #
- # @return [QueryApi]
- def query_api
- @query_api ||= influx_client.create_query_api
- end
-
- # InfluxDb client
- #
- # @return [InfluxDB2::Client]
- def influx_client
- @influx_client ||= InfluxDB2::Client.new(
- influxdb_url,
- influxdb_token,
- bucket: influxdb_bucket,
- org: "gitlab-qa",
- precision: InfluxDB2::WritePrecision::NANOSECOND
- )
- end
-
# Slack notifier
#
# @return [Slack::Notifier]
diff --git a/qa/qa/tools/test_resource_data_processor.rb b/qa/qa/tools/test_resource_data_processor.rb
index 965919dc516..a86c94b4914 100644
--- a/qa/qa/tools/test_resource_data_processor.rb
+++ b/qa/qa/tools/test_resource_data_processor.rb
@@ -49,10 +49,12 @@ module QA
# Otherwise create file and write data hash to file for the first time
#
# @return [void]
- def write_to_file
+ def write_to_file(suite_failed)
return if resources.empty?
- file = Pathname.new(Runtime::Env.test_resources_created_filepath)
+ start_str = suite_failed ? 'failed-test-resources' : 'test-resources'
+ file_name = Runtime::Env.running_in_ci? ? "#{start_str}-#{SecureRandom.hex(3)}.json" : "#{start_str}.json"
+ file = Pathname.new(File.join(Runtime::Path.qa_root, 'tmp', file_name))
FileUtils.mkdir_p(file.dirname)
data = resources.deep_dup
@@ -79,7 +81,7 @@ module QA
else
default
end
- rescue QA::Resource::Base::NoValueError
+ rescue QA::Resource::Base::NoValueError, QA::Resource::Errors::ResourceNotFoundError
default
end
end
diff --git a/qa/qa/tools/test_resources_handler.rb b/qa/qa/tools/test_resources_handler.rb
new file mode 100644
index 00000000000..476f87fff6b
--- /dev/null
+++ b/qa/qa/tools/test_resources_handler.rb
@@ -0,0 +1,180 @@
+# frozen_string_literal: true
+
+require "fog/google"
+
+# This script handles resources created during E2E test runs
+#
+# Delete: find all matching file pattern, read file and delete resources
+# rake test_resources:delete[<file_pattern>]
+#
+# Upload: find all matching file pattern for failed test resources
+# upload these files to GCS bucket `failed-test-resources` under specific environment name
+# rake test_resources:upload[<file_pattern>,<ci_project_name>]
+#
+# Download: download JSON files under a given environment name (bucket directory)
+# save to local under `tmp/`
+# rake test_resources:download[<ci_project_name>]
+#
+# Required environment variables:
+# GITLAB_ADDRESS, required for delete task
+# GITLAB_QA_ACCESS_TOKEN, required for delete task
+# QA_TEST_RESOURCES_FILE_PATTERN, optional for delete task, required for upload task
+# QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS, required for upload task or download task
+
+module QA
+ module Tools
+ class TestResourcesHandler
+ include Support::API
+
+ IGNORED_RESOURCES = [
+ 'QA::Resource::PersonalAccessToken',
+ 'QA::Resource::CiVariable',
+ 'QA::Resource::Repository::Commit',
+ 'QA::EE::Resource::GroupIteration',
+ 'QA::EE::Resource::Settings::Elasticsearch'
+ ].freeze
+
+ PROJECT = 'gitlab-qa-resources'
+ BUCKET = 'failed-test-resources'
+
+ def initialize(file_pattern = nil)
+ @file_pattern = file_pattern
+ end
+
+ def run_delete
+ failures = files.flat_map do |file|
+ resources = read_file(file)
+ next if resources.nil?
+
+ filtered_resources = filter_resources(resources)
+ delete_resources(filtered_resources)
+ end
+
+ return puts "\nDone" if failures.empty?
+
+ puts "\nFailed to delete #{failures.size} resources:\n"
+ puts failures
+ end
+
+ # Upload resources from failed test suites to GCS bucket
+ # Files are organized by environment in which tests were executed
+ #
+ # E.g: staging/failed-test-resources-<randomhex>.json
+ def upload(ci_project_name)
+ return puts "\nNothing to upload!" if files.empty?
+
+ files.each do |file|
+ file_name = "#{ci_project_name}/#{file.split('/').last}"
+ Runtime::Logger.info("Uploading #{file_name}...")
+ gcs_storage.put_object(BUCKET, file_name, File.read(file))
+ end
+
+ puts "\nDone"
+ end
+
+ # Download files from GCS bucket by environment name
+ # Delete the files afterward
+ def download(ci_project_name)
+ files_list = gcs_storage.list_objects(BUCKET, prefix: ci_project_name).items.each_with_object([]) do |obj, arr|
+ arr << obj.name
+ end
+
+ return puts "\nNothing to download!" if files_list.empty?
+
+ files_list.each do |file_name|
+ local_path = "tmp/#{file_name.split('/').last}"
+ Runtime::Logger.info("Downloading #{file_name} to #{local_path}")
+ file = gcs_storage.get_object(BUCKET, file_name)
+ File.write(local_path, file[:body])
+
+ Runtime::Logger.info("Deleting #{file_name} from bucket")
+ gcs_storage.delete_object(BUCKET, file_name)
+ end
+
+ puts "\nDone"
+ end
+
+ private
+
+ def files
+ Runtime::Logger.info('Gathering JSON files...')
+ files = Dir.glob(@file_pattern)
+ abort("There is no file with this pattern #{@file_pattern}") if files.empty?
+
+ files.reject! { |file| File.zero?(file) }
+
+ files
+ end
+
+ def read_file(file)
+ JSON.parse(File.read(file))
+ rescue JSON::ParserError
+ Runtime::Logger.error("Failed to read #{file} - Invalid format")
+ nil
+ end
+
+ def filter_resources(resources)
+ Runtime::Logger.info('Filtering resources - Only keep deletable resources...')
+
+ transformed_values = resources.transform_values! do |v|
+ v.reject do |attributes|
+ attributes['info'] == "with full_path 'gitlab-qa-sandbox-group'" ||
+ attributes['http_method'] == 'get' && !attributes['info']&.include?("with username 'qa-") ||
+ attributes['api_path'] == 'Cannot find resource API path'
+ end
+ end
+
+ transformed_values.reject! { |k, v| v.empty? || IGNORED_RESOURCES.include?(k) }
+ end
+
+ def delete_resources(resources)
+ Runtime::Logger.info('Nothing to delete.') && return if resources.nil?
+
+ resources.each_with_object([]) do |(key, value), failures|
+ value.each do |resource|
+ next if resource_not_found?(resource['api_path'])
+
+ resource_info = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}"
+ delete_response = delete(Runtime::API::Request.new(api_client, resource['api_path']).url)
+
+ if delete_response.code == 202 || delete_response.code == 204
+ Runtime::Logger.info("Deleting #{resource_info}... SUCCESS")
+ else
+ Runtime::Logger.info("Deleting #{resource_info}... FAILED")
+ failures << resource_info
+ end
+ end
+ end
+ end
+
+ def resource_not_found?(api_path)
+ # if api path contains param "?hard_delete=<boolean>", remove it
+ get(Runtime::API::Request.new(api_client, api_path.split('?').first).url).code.eql? 404
+ end
+
+ def api_client
+ abort("\nPlease provide GITLAB_ADDRESS") unless ENV['GITLAB_ADDRESS']
+ abort("\nPlease provide GITLAB_QA_ACCESS_TOKEN") unless ENV['GITLAB_QA_ACCESS_TOKEN']
+
+ @api_client ||= Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN'])
+ end
+
+ def gcs_storage
+ @gcs_storage ||= Fog::Storage::Google.new(
+ google_project: PROJECT,
+ **(File.exist?(json_key) ? { google_json_key_location: json_key } : { google_json_key_string: json_key })
+ )
+ rescue StandardError => e
+ abort("\nThere might be something wrong with the JSON key file - [ERROR] #{e}")
+ end
+
+ # Path to GCS service account json key file
+ # Or the content of the key file as a hash
+ def json_key
+ abort("\nPlease provide QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS") unless ENV['QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS']
+
+ @json_key ||= ENV["QA_FAILED_TEST_RESOURCES_GCS_CREDENTIALS"]
+ end
+ end
+ end
+end
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 98326ecd343..054332eea29 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -4,11 +4,10 @@ require 'capybara/dsl'
RSpec.describe QA::Support::Page::Logging do
let(:page) { double.as_null_object }
+ let(:logger) { Gitlab::QA::TestLogger.logger(level: ::Logger::DEBUG, source: 'QA Tests') }
before do
- logger = ::Logger.new $stdout
- logger.level = ::Logger::DEBUG
- QA::Runtime::Logger.logger = logger
+ allow(QA::Runtime::Logger).to receive(:logger).and_return(logger)
allow(Capybara).to receive(:current_session).and_return(page)
allow(page).to receive(:find).and_return(page)
@@ -59,7 +58,7 @@ RSpec.describe QA::Support::Page::Logging do
it 'logs find_element with class' do
expect { subject.find_element(:element, class: 'active') }
- .to output(/finding :element with args {:class=>\"active\"}/).to_stdout_from_any_process
+ .to output(/finding :element with args {:class=>"active"}/).to_stdout_from_any_process
end
it 'logs click_element' do
@@ -74,40 +73,40 @@ RSpec.describe QA::Support::Page::Logging do
it 'logs has_element?' do
expect { subject.has_element?(:element) }
- .to output(/has_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
+ .to output(/has_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process
end
it 'logs has_element? with text' do
expect { subject.has_element?(:element, text: "some text") }
- .to output(/has_element\? :element with text \"some text\" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
+ .to output(/has_element\? :element with text "some text" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process
end
it 'logs has_no_element?' do
allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element) }
- .to output(/has_no_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
+ .to output(/has_no_element\? :element \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process
end
it 'logs has_no_element? with text' do
allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element, text: "more text") }
- .to output(/has_no_element\? :element with text \"more text\" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/).to_stdout_from_any_process
+ .to output(/has_no_element\? :element with text "more text" \(wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned: true/o).to_stdout_from_any_process
end
it 'logs has_text?' do
allow(page).to receive(:has_text?).and_return(true)
expect { subject.has_text? 'foo' }
- .to output(/has_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/).to_stdout_from_any_process
+ .to output(/has_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/o).to_stdout_from_any_process
end
it 'logs has_no_text?' do
allow(page).to receive(:has_no_text?).with('foo', any_args).and_return(true)
expect { subject.has_no_text? 'foo' }
- .to output(/has_no_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/).to_stdout_from_any_process
+ .to output(/has_no_text\?\('foo', wait: #{QA::Runtime::Browser::CAPYBARA_MAX_WAIT_TIME}\) returned true/o).to_stdout_from_any_process
end
it 'logs finished_loading?' do
diff --git a/qa/spec/resource/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb
index 69a95c92332..ec9907916eb 100644
--- a/qa/spec/resource/api_fabricator_spec.rb
+++ b/qa/spec/resource/api_fabricator_spec.rb
@@ -108,15 +108,58 @@ RSpec.describe QA::Resource::ApiFabricator do
context 'when the POST fails' do
let(:post_response) { { error: "Name already taken." } }
- let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json) }
+ let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json, headers: {}) }
it 'raises a ResourceFabricationFailedError exception' do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
+ allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(nil)
- expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.")
+ expect { subject.fabricate_via_api! }.to raise_error do |error|
+ expect(error.class).to eql(described_class::ResourceFabricationFailedError)
+ expect(error.to_s).to eql(<<~ERROR.chomp)
+ Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.\n
+ ERROR
+ end
expect(subject.api_resource).to be_nil
end
+
+ it 'logs a correlation id' do
+ response = double('Raw POST response', code: 400, body: post_response.to_json, headers: { x_request_id: 'foobar' })
+ allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(nil)
+
+ expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
+ expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(response)
+
+ expect { subject.fabricate_via_api! }.to raise_error do |error|
+ expect(error.class).to eql(described_class::ResourceFabricationFailedError)
+ expect(error.to_s).to eql(<<~ERROR.chomp)
+ Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.
+ Correlation Id: foobar
+ ERROR
+ end
+ end
+
+ it 'logs a sentry url from staging' do
+ response = double('Raw POST response', code: 400, body: post_response.to_json, headers: { x_request_id: 'foobar' })
+ cookies = [{ name: 'Foo', value: 'Bar' }, { name: 'gitlab_canary', value: 'true' }]
+
+ allow(Capybara.current_session).to receive_message_chain(:driver, :browser, :manage, :all_cookies).and_return(cookies)
+ allow(QA::Runtime::Scenario).to receive(:attributes).and_return({ gitlab_address: 'https://staging.gitlab.com' })
+
+ expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
+ expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(response)
+
+ expect { subject.fabricate_via_api! }.to raise_error do |error|
+ expect(error.class).to eql(described_class::ResourceFabricationFailedError)
+ expect(error.to_s).to eql(<<~ERROR.chomp)
+ Fabrication of FooBarResource using the API failed (400) with `#{raw_post}`.
+ Correlation Id: foobar
+ Sentry Url: https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny&query=correlation_id%3A%22foobar%22
+ Kibana Url: https://nonprod-log.gitlab.net/app/discover#/?_a=(query:(language:kuery,query:'json.correlation_id%20:%20foobar'))
+ ERROR
+ end
+ end
end
end
diff --git a/qa/spec/resource/reusable_collection_spec.rb b/qa/spec/resource/reusable_collection_spec.rb
index 9116462b396..cb2df6931d0 100644
--- a/qa/spec/resource/reusable_collection_spec.rb
+++ b/qa/spec/resource/reusable_collection_spec.rb
@@ -20,6 +20,10 @@ RSpec.describe QA::Resource::ReusableCollection do
end
def exists?() end
+
+ def reload!
+ Struct.new(:api_resource).new({ marked_for_deletion_on: false })
+ end
end
end
@@ -88,8 +92,22 @@ RSpec.describe QA::Resource::ReusableCollection do
it 'removes each instance of each resource class' do
described_class.remove_all_via_api!
- expect(a_resource_instance.removed).to be true
- expect(another_resource_instance.removed).to be true
+ expect(a_resource_instance.removed).to be_truthy
+ expect(another_resource_instance.removed).to be_truthy
+ end
+
+ context 'when a resource is marked for deletion' do
+ before do
+ marked_for_deletion = Struct.new(:api_resource).new({ marked_for_deletion_on: true })
+
+ allow(a_resource_instance).to receive(:reload!).and_return(marked_for_deletion)
+ allow(another_resource_instance).to receive(:reload!).and_return(marked_for_deletion)
+ end
+
+ it 'does not remove the resource' do
+ expect(a_resource_instance.removed).to be_falsey
+ expect(another_resource_instance.removed).to be_falsy
+ end
end
end
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 0f752ad96b7..22603497019 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -360,36 +360,4 @@ RSpec.describe QA::Runtime::Env do
end
end
end
-
- describe '.test_resources_created_filepath' do
- context 'when not in CI' do
- before do
- allow(described_class).to receive(:running_in_ci?).and_return(false)
- end
-
- it 'returns default path if QA_TEST_RESOURCES_CREATED_FILEPATH is not defined' do
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', nil)
-
- expect(described_class.test_resources_created_filepath).to include('tmp/test-resources.json')
- end
-
- it 'returns path if QA_TEST_RESOURCES_CREATED_FILEPATH is defined' do
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', 'path/to_file')
-
- expect(described_class.test_resources_created_filepath).to eq('path/to_file')
- end
- end
-
- context 'when in CI' do
- before do
- allow(described_class).to receive(:running_in_ci?).and_return(true)
- allow(SecureRandom).to receive(:hex).with(3).and_return('abc123')
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', nil)
- end
-
- it 'returns path with random hex in file name' do
- expect(described_class.test_resources_created_filepath).to include('tmp/test-resources-abc123.json')
- end
- end
- end
end
diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb
index a888bf1452b..f0fcfa0564e 100644
--- a/qa/spec/runtime/logger_spec.rb
+++ b/qa/spec/runtime/logger_spec.rb
@@ -1,33 +1,7 @@
# frozen_string_literal: true
RSpec.describe QA::Runtime::Logger do
- before do
- logger = Logger.new $stdout
- logger.level = ::Logger::DEBUG
- described_class.logger = logger
- end
-
- it 'logs debug' do
- expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process
- end
-
- it 'logs info' do
- expect { described_class.info('test') }.to output(/INFO -- : test/).to_stdout_from_any_process
- end
-
- it 'logs warn' do
- expect { described_class.warn('test') }.to output(/WARN -- : test/).to_stdout_from_any_process
- end
-
- it 'logs error' do
- expect { described_class.error('test') }.to output(/ERROR -- : test/).to_stdout_from_any_process
- end
-
- it 'logs fatal' do
- expect { described_class.fatal('test') }.to output(/FATAL -- : test/).to_stdout_from_any_process
- end
-
- it 'logs unknown' do
- expect { described_class.unknown('test') }.to output(/ANY -- : test/).to_stdout_from_any_process
+ it 'returns logger instance' do
+ expect(described_class.logger).to be_an_instance_of(::Logger)
end
end
diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb
index 2a6acd6d014..655b0088feb 100644
--- a/qa/spec/spec_helper.rb
+++ b/qa/spec/spec_helper.rb
@@ -2,12 +2,6 @@
require_relative '../qa'
-require 'securerandom'
-require 'pathname'
-require 'active_support/core_ext/hash'
-require 'active_support/core_ext/object/blank'
-require 'rainbow/refinement'
-
require_relative 'qa_deprecation_toolkit_env'
QaDeprecationToolkitEnv.configure!
@@ -36,7 +30,7 @@ RSpec.configure do |config|
end
config.prepend_before do |example|
- QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n")
+ QA::Runtime::Logger.info("Starting test: #{Rainbow(example.full_description).bright}")
QA::Runtime::Example.current = example
# Reset fabrication counters tracked in resource base
@@ -75,14 +69,15 @@ RSpec.configure do |config|
config.after(:suite) do |suite|
# Write all test created resources to JSON file
- QA::Tools::TestResourceDataProcessor.write_to_file
+ QA::Tools::TestResourceDataProcessor.write_to_file(suite.reporter.failed_examples.any?)
# If requested, confirm that resources were used appropriately (e.g., not left with changes that interfere with
# further reuse)
QA::Resource::ReusableCollection.validate_resource_reuse if QA::Runtime::Env.validate_resource_reuse?
# If any tests failed, leave the resources behind to help troubleshoot, otherwise remove them.
- QA::Resource::ReusableCollection.remove_all_via_api! unless suite.reporter.failed_examples.present?
+ # Do not remove the shared resource on live environments
+ QA::Resource::ReusableCollection.remove_all_via_api! if !suite.reporter.failed_examples.present? && !QA::Runtime::Env.running_on_dot_com?
end
config.append_after(:suite) do
diff --git a/qa/spec/specs/allure_report_spec.rb b/qa/spec/specs/allure_report_spec.rb
index 06b09106140..86ceaf51cbb 100644
--- a/qa/spec/specs/allure_report_spec.rb
+++ b/qa/spec/specs/allure_report_spec.rb
@@ -76,9 +76,10 @@ describe QA::Runtime::AllureReport do
end
it 'adds rspec and metadata formatter' do
+ expect(rspec_config).to have_received(:add_formatter).with(
+ QA::Support::Formatters::AllureMetadataFormatter
+ ).ordered
expect(rspec_config).to have_received(:add_formatter).with(AllureRspecFormatter).ordered
- expect(rspec_config).to have_received(:add_formatter)
- .with(QA::Support::Formatters::AllureMetadataFormatter).ordered
end
it 'configures attachments saving' do
diff --git a/qa/spec/support/formatters/allure_metadata_formatter_spec.rb b/qa/spec/support/formatters/allure_metadata_formatter_spec.rb
index 631d2eda54f..d84e190fd56 100644
--- a/qa/spec/support/formatters/allure_metadata_formatter_spec.rb
+++ b/qa/spec/support/formatters/allure_metadata_formatter_spec.rb
@@ -14,6 +14,7 @@ describe QA::Support::Formatters::AllureMetadataFormatter do
add_link: nil,
attempts: 0,
file_path: 'file/path/spec.rb',
+ execution_result: instance_double("RSpec::Core::Example::ExecutionResult", status: :passed),
metadata: {
testcase: 'testcase',
quarantine: { issue: 'issue' }
@@ -31,7 +32,7 @@ describe QA::Support::Formatters::AllureMetadataFormatter do
end
it "adds additional data to report" do
- formatter.example_started(rspec_example_notification)
+ formatter.example_finished(rspec_example_notification)
aggregate_failures do
expect(rspec_example).to have_received(:issue).with('Quarantine issue', 'issue')
diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_stats_formatter_spec.rb
index 84fc3b83185..518c7407ba6 100644
--- a/qa/spec/support/formatters/test_stats_formatter_spec.rb
+++ b/qa/spec/support/formatters/test_stats_formatter_spec.rb
@@ -60,7 +60,8 @@ describe QA::Support::Formatters::TestStatsFormatter do
retry_attempts: 0,
job_url: ci_job_url,
pipeline_url: ci_pipeline_url,
- pipeline_id: ci_pipeline_id
+ pipeline_id: ci_pipeline_id,
+ merge_request_iid: nil
}
}
end
@@ -157,6 +158,23 @@ describe QA::Support::Formatters::TestStatsFormatter do
end
end
+ context 'with context quarantined spec' do
+ let(:quarantined) { 'false' }
+
+ it 'exports data to influxdb with correct qurantine tag' do
+ run_spec do
+ it(
+ 'spec',
+ quarantine: { only: { job: 'praefect' } },
+ testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234'
+ ) {}
+ end
+
+ expect(influx_write_api).to have_received(:write).once
+ expect(influx_write_api).to have_received(:write).with(data: [data])
+ end
+ end
+
context 'with staging full run' do
let(:run_type) { 'staging-full' }
diff --git a/qa/spec/support/loglinking_spec.rb b/qa/spec/support/loglinking_spec.rb
new file mode 100644
index 00000000000..cba8a139767
--- /dev/null
+++ b/qa/spec/support/loglinking_spec.rb
@@ -0,0 +1,185 @@
+# frozen_string_literal: true
+
+RSpec.describe QA::Support::Loglinking do
+ describe '.failure_metadata' do
+ context 'return nil string' do
+ it 'if correlation_id is empty' do
+ expect(QA::Support::Loglinking.failure_metadata('')).to eq(nil)
+ end
+ it 'if correlation_id is nil' do
+ expect(QA::Support::Loglinking.failure_metadata(nil)).to eq(nil)
+ end
+ end
+
+ context 'return error string' do
+ it 'with sentry URL' do
+ allow(QA::Support::Loglinking).to receive(:sentry_url).and_return('https://sentry.address/?environment=bar')
+ allow(QA::Support::Loglinking).to receive(:kibana_url).and_return(nil)
+
+ expect(QA::Support::Loglinking.failure_metadata('foo123')).to eql(<<~ERROR.chomp)
+ Correlation Id: foo123
+ Sentry Url: https://sentry.address/?environment=bar&query=correlation_id%3A%22foo123%22
+ ERROR
+ end
+
+ it 'with kibana URL' do
+ allow(QA::Support::Loglinking).to receive(:sentry_url).and_return(nil)
+ allow(QA::Support::Loglinking).to receive(:kibana_url).and_return('https://kibana.address/')
+
+ expect(QA::Support::Loglinking.failure_metadata('foo123')).to eql(<<~ERROR.chomp)
+ Correlation Id: foo123
+ Kibana Url: https://kibana.address/app/discover#/?_a=(query:(language:kuery,query:'json.correlation_id%20:%20foo123'))
+ ERROR
+ end
+ end
+ end
+
+ describe '.sentry_url' do
+ let(:url_hash) do
+ {
+ :staging => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg',
+ :staging_canary => 'https://sentry.gitlab.net/gitlab/staginggitlabcom/?environment=gstg-cny',
+ :staging_ref => 'https://sentry.gitlab.net/gitlab/staging-ref/?environment=gstg-ref',
+ :pre => 'https://sentry.gitlab.net/gitlab/pregitlabcom/?environment=pre',
+ :canary => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd',
+ :production => 'https://sentry.gitlab.net/gitlab/gitlabcom/?environment=gprd-cny',
+ :foo => nil,
+ nil => nil
+ }
+ end
+
+ it 'returns sentry URL if environment found' do
+ url_hash.each do |environment, url|
+ allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(environment)
+
+ expect(QA::Support::Loglinking.sentry_url).to eq(url)
+ end
+ end
+ end
+
+ describe '.kibana_url' do
+ let(:url_hash) do
+ {
+ :staging => 'https://nonprod-log.gitlab.net/',
+ :staging_canary => 'https://nonprod-log.gitlab.net/',
+ :staging_ref => nil,
+ :pre => nil,
+ :canary => 'https://log.gprd.gitlab.net/',
+ :production => 'https://log.gprd.gitlab.net/',
+ :foo => nil,
+ nil => nil
+ }
+ end
+
+ it 'returns kibana URL if environment found' do
+ url_hash.each do |environment, url|
+ allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(environment)
+
+ expect(QA::Support::Loglinking.kibana_url).to eq(url)
+ end
+ end
+ end
+
+ describe '.logging_environment' do
+ let(:staging_address) { 'https://staging.gitlab.com' }
+ let(:staging_ref_address) { 'https://staging-ref.gitlab.com' }
+ let(:production_address) { 'https://www.gitlab.com' }
+ let(:pre_prod_address) { 'https://pre.gitlab.com' }
+ let(:logging_env_array) do
+ [
+ {
+ address: staging_address,
+ canary: false,
+ expected_env: :staging
+ },
+ {
+ address: staging_address,
+ canary: true,
+ expected_env: :staging_canary
+ },
+ {
+ address: staging_ref_address,
+ canary: true,
+ expected_env: :staging_ref
+ },
+ {
+ address: production_address,
+ canary: false,
+ expected_env: :production
+ },
+ {
+ address: production_address,
+ canary: true,
+ expected_env: :canary
+ },
+ {
+ address: pre_prod_address,
+ canary: true,
+ expected_env: :pre
+ },
+ {
+ address: 'https://foo.com',
+ canary: true,
+ expected_env: nil
+ }
+ ]
+ end
+
+ it 'returns logging environment if environment found' do
+ logging_env_array.each do |logging_env_hash|
+ allow(QA::Runtime::Scenario).to receive(:attributes).and_return({ gitlab_address: logging_env_hash[:address] })
+ allow(QA::Support::Loglinking).to receive(:canary?).and_return(logging_env_hash[:canary])
+
+ expect(QA::Support::Loglinking.logging_environment).to eq(logging_env_hash[:expected_env])
+ end
+ end
+ end
+
+ describe '.logging_environment?' do
+ context 'returns boolean' do
+ it 'returns true if logging_environment is not nil' do
+ allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(:staging)
+
+ expect(QA::Support::Loglinking.logging_environment?).to eq(true)
+ end
+
+ it 'returns false if logging_environment is nil' do
+ allow(QA::Support::Loglinking).to receive(:logging_environment).and_return(nil)
+
+ expect(QA::Support::Loglinking.logging_environment?).to eq(false)
+ end
+ end
+ end
+
+ describe '.cookies' do
+ let(:cookies) { [{ name: 'Foo', value: 'Bar' }, { name: 'gitlab_canary', value: 'true' }] }
+
+ it 'returns browser cookies' do
+ allow(Capybara.current_session).to receive_message_chain(:driver, :browser, :manage, :all_cookies).and_return(cookies)
+
+ expect(QA::Support::Loglinking.cookies).to eq({ "Foo" => { name: "Foo", value: "Bar" }, "gitlab_canary" => { name: "gitlab_canary", value: "true" } })
+ end
+ end
+
+ describe '.canary?' do
+ context 'gitlab_canary cookie is present' do
+ it 'and true returns true' do
+ allow(QA::Support::Loglinking).to receive(:cookies).and_return({ 'gitlab_canary' => { name: 'gitlab_canary', value: 'true' } })
+
+ expect(QA::Support::Loglinking.canary?).to eq(true)
+ end
+ it 'and not true returns false' do
+ allow(QA::Support::Loglinking).to receive(:cookies).and_return({ 'gitlab_canary' => { name: 'gitlab_canary', value: 'false' } })
+
+ expect(QA::Support::Loglinking.canary?).to eq(false)
+ end
+ end
+ context 'gitlab_canary cookie is not present' do
+ it 'returns false' do
+ allow(QA::Support::Loglinking).to receive(:cookies).and_return({ 'foo' => { name: 'foo', path: '/pathname' } })
+
+ expect(QA::Support::Loglinking.canary?).to eq(false)
+ end
+ end
+ end
+end
diff --git a/qa/spec/support/page_error_checker_spec.rb b/qa/spec/support/page_error_checker_spec.rb
index 764b6110e08..b9b085fa7b9 100644
--- a/qa/spec/support/page_error_checker_spec.rb
+++ b/qa/spec/support/page_error_checker_spec.rb
@@ -8,16 +8,28 @@ RSpec.describe QA::Support::PageErrorChecker do
describe '.report!' do
context 'reports errors' do
let(:expected_chrome_error) do
+ "Error Code 500\n\n"\
"chrome errors\n\n"\
- "Path: #{test_path}"
+ "Path: #{test_path}\n\n"\
+ "Logging: foo123"
end
let(:expected_basic_error) do
+ "Error Code 500\n\n"\
+ "foo status\n\n"\
+ "Path: #{test_path}\n\n"\
+ "Logging: foo123"
+ end
+
+ let(:expected_basic_404) do
+ "Error Code 404\n\n"\
"foo status\n\n"\
"Path: #{test_path}"
end
it 'reports error message on chrome browser' do
+ allow(QA::Support::PageErrorChecker).to receive(:parse_five_c_page_request_id).and_return('foo123')
+ allow(QA::Support::Loglinking).to receive(:failure_metadata).with('foo123').and_return('Logging: foo123')
allow(QA::Support::PageErrorChecker).to receive(:return_chrome_errors).and_return('chrome errors')
allow(page).to receive(:current_path).and_return(test_path)
allow(QA::Runtime::Env).to receive(:browser).and_return(:chrome)
@@ -26,12 +38,64 @@ RSpec.describe QA::Support::PageErrorChecker do
end
it 'reports basic message on non-chrome browser' do
+ allow(QA::Support::PageErrorChecker).to receive(:parse_five_c_page_request_id).and_return('foo123')
+ allow(QA::Support::Loglinking).to receive(:failure_metadata).with('foo123').and_return('Logging: foo123')
allow(QA::Support::PageErrorChecker).to receive(:status_code_report).and_return('foo status')
allow(page).to receive(:current_path).and_return(test_path)
allow(QA::Runtime::Env).to receive(:browser).and_return(:firefox)
expect { QA::Support::PageErrorChecker.report!(page, 500) }.to raise_error(RuntimeError, expected_basic_error)
end
+
+ it 'does not report failure metadata on non 500 error' do
+ allow(QA::Support::PageErrorChecker).to receive(:parse_five_c_page_request_id).and_return('foo123')
+
+ expect(QA::Support::Loglinking).not_to receive(:failure_metadata)
+
+ allow(QA::Support::PageErrorChecker).to receive(:status_code_report).and_return('foo status')
+ allow(page).to receive(:current_path).and_return(test_path)
+ allow(QA::Runtime::Env).to receive(:browser).and_return(:firefox)
+
+ expect { QA::Support::PageErrorChecker.report!(page, 404) }.to raise_error(RuntimeError, expected_basic_404)
+ end
+ end
+ end
+
+ describe '.parse_five_c_page_request_id' do
+ context 'parse correlation ID' do
+ require 'nokogiri'
+ before do
+ nokogiri_parse = Class.new do
+ def self.parse(str)
+ Nokogiri::HTML.parse(str)
+ end
+ end
+ stub_const('NokogiriParse', nokogiri_parse)
+ end
+ let(:error_500_str) do
+ "<html><body><div><p><code>"\
+ "req678"\
+ "</code></p></div></body></html>"
+ end
+
+ let(:error_500_no_code_str) do
+ "<html><body>"\
+ "The code you are looking for is not here"\
+ "</body></html>"
+ end
+
+ it 'returns code is present' do
+ allow(page).to receive(:html).and_return(error_500_str)
+ allow(Nokogiri::HTML).to receive(:parse).with(error_500_str).and_return(NokogiriParse.parse(error_500_str))
+
+ expect(QA::Support::PageErrorChecker.parse_five_c_page_request_id(page).to_str).to eq('req678')
+ end
+ it 'returns nil if not present' do
+ allow(page).to receive(:html).and_return(error_500_no_code_str)
+ allow(Nokogiri::HTML).to receive(:parse).with(error_500_no_code_str).and_return(NokogiriParse.parse(error_500_no_code_str))
+
+ expect(QA::Support::PageErrorChecker.parse_five_c_page_request_id(page)).to be_nil
+ end
end
end
diff --git a/qa/spec/support/repeater_spec.rb b/qa/spec/support/repeater_spec.rb
index 4fa3bcde5e7..96e780fc9bd 100644
--- a/qa/spec/support/repeater_spec.rb
+++ b/qa/spec/support/repeater_spec.rb
@@ -3,12 +3,6 @@
require 'active_support/core_ext/integer/time'
RSpec.describe QA::Support::Repeater do
- before do
- logger = ::Logger.new $stdout
- logger.level = ::Logger::DEBUG
- QA::Runtime::Logger.logger = logger
- end
-
subject do
Module.new do
extend QA::Support::Repeater
diff --git a/qa/spec/tools/reliable_report_spec.rb b/qa/spec/tools/reliable_report_spec.rb
index 85b2590d3aa..318b0833f62 100644
--- a/qa/spec/tools/reliable_report_spec.rb
+++ b/qa/spec/tools/reliable_report_spec.rb
@@ -167,7 +167,7 @@ describe QA::Tools::ReliableReport do
payload: {
title: "Reliable e2e test report",
description: issue_body,
- labels: "Quality,test,type::maintenance,reliable test report,automation:devops-mapping-disable"
+ labels: "Quality,test,type::maintenance,reliable test report,automation:ml"
}
)
expect(slack_notifier).to have_received(:post).with(
diff --git a/qa/spec/tools/test_resources_data_processor_spec.rb b/qa/spec/tools/test_resources_data_processor_spec.rb
index 5117d1d367f..2ae43974a0c 100644
--- a/qa/spec/tools/test_resources_data_processor_spec.rb
+++ b/qa/spec/tools/test_resources_data_processor_spec.rb
@@ -43,18 +43,30 @@ RSpec.describe QA::Tools::TestResourceDataProcessor do
end
describe '.write_to_file' do
- let(:resources_file) { Pathname.new(Faker::File.file_name(dir: 'tmp', ext: 'json')) }
+ using RSpec::Parameterized::TableSyntax
- before do
- stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', resources_file)
-
- allow(File).to receive(:write)
+ where(:ci, :suite_failed, :file_path) do
+ true | true | 'root/tmp/failed-test-resources-random.json'
+ true | false | 'root/tmp/test-resources-random.json'
+ false | true | 'root/tmp/failed-test-resources.json'
+ false | false | 'root/tmp/test-resources.json'
end
- it 'writes applicable resources to file' do
- processor.write_to_file
+ with_them do
+ let(:resources_file) { Pathname.new(file_path) }
+
+ before do
+ allow(QA::Runtime::Env).to receive(:running_in_ci?).and_return(ci)
+ allow(File).to receive(:write)
+ allow(QA::Runtime::Path).to receive(:qa_root).and_return('root')
+ allow(SecureRandom).to receive(:hex).with(any_args).and_return('random')
+ end
+
+ it 'writes applicable resources to file' do
+ processor.write_to_file(suite_failed)
- expect(File).to have_received(:write).with(resources_file, JSON.pretty_generate(result))
+ expect(File).to have_received(:write).with(resources_file, JSON.pretty_generate(result))
+ end
end
end
end
diff --git a/qa/tasks/contracts.rake b/qa/tasks/contracts.rake
new file mode 100644
index 00000000000..682ec0e2e21
--- /dev/null
+++ b/qa/tasks/contracts.rake
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'pact/tasks/verification_task'
+
+contracts = File.expand_path('../contracts', __dir__)
+provider = File.expand_path('provider', contracts)
+
+# rubocop:disable Rails/RakeEnvironment
+namespace :contracts do
+ namespace :mr do
+ Pact::VerificationTask.new(:metadata) do |pact|
+ pact.uri(
+ "#{contracts}/contracts/merge_request_page-merge_request_metadata_endpoint.json",
+ pact_helper: "#{provider}/spec/metadata_helper.rb"
+ )
+ end
+
+ Pact::VerificationTask.new(:discussions) do |pact|
+ pact.uri(
+ "#{contracts}/contracts/merge_request_page-merge_request_discussions_endpoint.json",
+ pact_helper: "#{provider}/spec/discussions_helper.rb"
+ )
+ end
+
+ Pact::VerificationTask.new(:diffs) do |pact|
+ pact.uri(
+ "#{contracts}/contracts/merge_request_page-merge_request_diffs_endpoint.json",
+ pact_helper: "#{provider}/spec/diffs_helper.rb"
+ )
+ end
+
+ desc 'Run all merge request contract tests'
+ task 'test:merge_request', :contract_mr do |_t, arg|
+ raise(ArgumentError, 'Merge request contract tests require contract_mr to be set') unless arg[:contract_mr]
+
+ ENV['CONTRACT_MR'] = arg[:contract_mr]
+ errors = %w[metadata discussions diffs].each_with_object([]) do |task, err|
+ Rake::Task["contracts:mr:pact:verify:#{task}"].execute
+ rescue StandardError, SystemExit
+ err << "contracts:mr:pact:verify:#{task}"
+ end
+
+ raise StandardError, "Errors in tasks #{errors.join(', ')}" unless errors.empty?
+ end
+ end
+end
+# rubocop:enable Rails/RakeEnvironment