diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-03-18 20:02:30 +0000 |
commit | 41fe97390ceddf945f3d967b8fdb3de4c66b7dea (patch) | |
tree | 9c8d89a8624828992f06d892cd2f43818ff5dcc8 /qa | |
parent | 0804d2dc31052fb45a1efecedc8e06ce9bc32862 (diff) | |
download | gitlab-ce-41fe97390ceddf945f3d967b8fdb3de4c66b7dea.tar.gz |
Add latest changes from gitlab-org/gitlab@14-9-stable-eev14.9.0-rc42
Diffstat (limited to 'qa')
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, @@ -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)<img src=x onerror=alert(1)>" + user.name = "QA User <img src=x onerror=alert(2)<img src=x onerror=alert(1)>" 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 |