diff options
Diffstat (limited to 'qa')
160 files changed, 2399 insertions, 537 deletions
diff --git a/qa/Gemfile b/qa/Gemfile index d946b22a0e0..f00f26a5482 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -1,7 +1,7 @@ source 'https://rubygems.org' gem 'gitlab-qa' -gem 'activesupport', '~> 6.0.3.1' # This should stay in sync with the root's Gemfile +gem 'activesupport', '~> 6.0.3.3' # This should stay in sync with the root's Gemfile gem 'capybara', '~> 3.29.0' gem 'capybara-screenshot', '~> 1.0.23' gem 'rake', '~> 12.3.3' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 2356c90a0af..6cdedc3834d 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.0.3.1) + activesupport (6.0.3.3) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 0.7, < 2) minitest (~> 5.1) @@ -38,7 +38,7 @@ GEM concord (0.1.5) adamantium (~> 0.2.0) equalizer (~> 0.0.9) - concurrent-ruby (1.1.6) + concurrent-ruby (1.1.7) debase (0.2.4.1) debase-ruby_core_source (>= 0.10.2) debase-ruby_core_source (0.10.6) @@ -52,7 +52,7 @@ GEM http-accept (1.7.0) http-cookie (1.0.3) domain_name (~> 0.5) - i18n (1.8.2) + i18n (1.8.5) concurrent-ruby (~> 1.0) ice_nine (0.11.2) knapsack (1.17.1) @@ -67,11 +67,11 @@ GEM mime-types-data (3.2020.0425) mini_mime (1.0.2) mini_portile2 (2.4.0) - minitest (5.14.1) + minitest (5.14.2) netrc (0.11.0) nokogiri (1.10.9) mini_portile2 (~> 2.4.0) - parallel (1.17.0) + parallel (1.19.2) parallel_tests (2.29.0) parallel parser (2.7.1.4) @@ -145,13 +145,13 @@ GEM procto (~> 0.0.2) xpath (3.2.0) nokogiri (~> 1.8) - zeitwerk (2.3.0) + zeitwerk (2.4.0) PLATFORMS ruby DEPENDENCIES - activesupport (~> 6.0.3.1) + activesupport (~> 6.0.3.3) airborne (~> 0.3.4) capybara (~> 3.29.0) capybara-screenshot (~> 1.0.23) diff --git a/qa/README.md b/qa/README.md index 7ed4d63a589..5070e1ee9bd 100644 --- a/qa/README.md +++ b/qa/README.md @@ -50,7 +50,7 @@ the browser to use. You will need to have Chrome (or Chromium) and - [Best practices](../doc/development/testing_guide/best_practices.md) - [Using page objects](../doc/development/testing_guide/end_to_end/page_objects.md) - [Guidelines](../doc/development/testing_guide/index.md) - - [Tests with special setup for local environemnts](../doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md) + - [Tests with special setup for local environments](../doc/development/testing_guide/end_to_end/running_tests_that_require_special_setup.md) ### Run the end-to-end tests in a local development environment diff --git a/qa/Rakefile b/qa/Rakefile index 844d8ff898d..1ecce8fdce9 100644 --- a/qa/Rakefile +++ b/qa/Rakefile @@ -2,6 +2,7 @@ require_relative 'qa/tools/revoke_all_personal_access_tokens' require_relative 'qa/tools/delete_subgroups' require_relative 'qa/tools/generate_perf_testdata' require_relative 'qa/tools/delete_test_ssh_keys' +require_relative 'qa/tools/initialize_gitlab_auth' desc "Revokes all personal access tokens" task :revoke_personal_access_tokens do @@ -13,6 +14,11 @@ task :delete_subgroups do QA::Tools::DeleteSubgroups.new.run end +desc "Initialize GitLab with an access token" +task :initialize_gitlab_auth, [:address] do |t, args| + QA::Tools::InitializeGitLabAuth.new(args).run +end + desc "Generate Performance Testdata" task :generate_perf_testdata, :type do |t, args| args.with_defaults(type: :all) @@ -18,6 +18,7 @@ module QA autoload :Project, 'qa/flow/project' autoload :Saml, 'qa/flow/saml' autoload :User, 'qa/flow/user' + autoload :MergeRequest, 'qa/flow/merge_request' end ## @@ -91,6 +92,7 @@ module QA autoload :UserGPG, 'qa/resource/user_gpg' autoload :Visibility, 'qa/resource/visibility' autoload :ProjectSnippet, 'qa/resource/project_snippet' + autoload :Design, 'qa/resource/design' module KubernetesCluster autoload :Base, 'qa/resource/kubernetes_cluster/base' @@ -190,6 +192,7 @@ module QA autoload :Projects, 'qa/page/dashboard/projects' autoload :Groups, 'qa/page/dashboard/groups' autoload :Welcome, 'qa/page/dashboard/welcome' + autoload :Todos, 'qa/page/dashboard/todos' module Snippet autoload :New, 'qa/page/dashboard/snippet/new' @@ -260,6 +263,7 @@ module QA module Pipeline autoload :Index, 'qa/page/project/pipeline/index' autoload :Show, 'qa/page/project/pipeline/show' + autoload :New, 'qa/page/project/pipeline/new' end module Tag @@ -371,6 +375,7 @@ module QA module Snippet autoload :New, 'qa/page/project/snippet/new' autoload :Show, 'qa/page/project/snippet/show' + autoload :Index, 'qa/page/project/snippet/index' end end @@ -590,10 +595,12 @@ module QA autoload :Api, 'qa/support/api' autoload :Dates, 'qa/support/dates' autoload :Repeater, 'qa/support/repeater' + autoload :Run, 'qa/support/run' autoload :Retrier, 'qa/support/retrier' autoload :Waiter, 'qa/support/waiter' autoload :WaitForRequests, 'qa/support/wait_for_requests' autoload :OTP, 'qa/support/otp' + autoload :SSH, 'qa/support/ssh' end end diff --git a/qa/spec/fixtures/banana_sample.gif b/qa/qa/fixtures/designs/banana_sample.gif Binary files differindex 1322ac92d14..1322ac92d14 100644 --- a/qa/spec/fixtures/banana_sample.gif +++ b/qa/qa/fixtures/designs/banana_sample.gif diff --git a/qa/qa/fixtures/designs/tanuki.jpg b/qa/qa/fixtures/designs/tanuki.jpg Binary files differnew file mode 100644 index 00000000000..f0df472663e --- /dev/null +++ b/qa/qa/fixtures/designs/tanuki.jpg diff --git a/qa/qa/fixtures/designs/update/tanuki.jpg b/qa/qa/fixtures/designs/update/tanuki.jpg Binary files differnew file mode 100644 index 00000000000..162beda6c7b --- /dev/null +++ b/qa/qa/fixtures/designs/update/tanuki.jpg diff --git a/qa/qa/fixtures/designs/values.png b/qa/qa/fixtures/designs/values.png Binary files differnew file mode 100644 index 00000000000..9ecb6e7b778 --- /dev/null +++ b/qa/qa/fixtures/designs/values.png diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb index d4d5cc2dcfc..d23d8eaf097 100644 --- a/qa/qa/flow/login.rb +++ b/qa/qa/flow/login.rb @@ -23,6 +23,7 @@ module QA end def sign_in(as: nil, address: :gitlab, skip_page_validation: false) + Page::Main::Menu.perform(&:sign_out) if Page::Main::Menu.perform(&:signed_in?) Runtime::Browser.visit(address, Page::Main::Login) Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: as, skip_page_validation: skip_page_validation) } end diff --git a/qa/qa/flow/merge_request.rb b/qa/qa/flow/merge_request.rb new file mode 100644 index 00000000000..c26140000fe --- /dev/null +++ b/qa/qa/flow/merge_request.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module QA + module Flow + module MergeRequest + module_function + + def enable_merge_trains + Page::Project::Menu.perform(&:go_to_general_settings) + Page::Project::Settings::Main.perform(&:expand_merge_requests_settings) + Page::Project::Settings::MergeRequest.perform(&:enable_merge_train) + end + end + end +end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 035946b8471..0f7e4fbbc97 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -2,10 +2,8 @@ require 'cgi' require 'uri' -require 'open3' require 'fileutils' require 'tmpdir' -require 'tempfile' require 'securerandom' module QA @@ -13,8 +11,7 @@ module QA class Repository include Scenario::Actable include Support::Repeater - - RepositoryCommandError = Class.new(StandardError) + include Support::Run attr_writer :use_lfs, :gpg_key_id attr_accessor :env_vars @@ -59,8 +56,12 @@ module QA self.username, self.password = default_credentials end + def use_default_identity + configure_identity('GitLab QA', 'root@gitlab.com') + end + def clone(opts = '') - clone_result = run("git clone #{opts} #{uri} ./", max_attempts: 3) + clone_result = run_git("git clone #{opts} #{uri} ./", max_attempts: 3) return clone_result.response unless clone_result.success? enable_lfs_result = enable_lfs if use_lfs? @@ -70,7 +71,7 @@ module QA def checkout(branch_name, new_branch: false) opts = new_branch ? '-b' : '' - run(%Q{git checkout #{opts} "#{branch_name}"}).to_s + run_git(%Q{git checkout #{opts} "#{branch_name}"}).to_s end def shallow_clone @@ -78,8 +79,8 @@ module QA end def configure_identity(name, email) - run(%Q{git config user.name "#{name}"}) - run(%Q{git config user.email #{email}}) + run_git(%Q{git config user.name "#{name}"}) + run_git(%Q{git config user.email #{email}}) end def commit_file(name, contents, message) @@ -93,83 +94,82 @@ module QA ::File.write(name, contents) if use_lfs? - git_lfs_track_result = run(%Q{git lfs track #{name} --lockable}) + git_lfs_track_result = run_git(%Q{git lfs track #{name} --lockable}) return git_lfs_track_result.response unless git_lfs_track_result.success? end - git_add_result = run(%Q{git add #{name}}) + git_add_result = run_git(%Q{git add #{name}}) git_lfs_track_result.to_s + git_add_result.to_s end def add_tag(tag_name) - run("git tag #{tag_name}").to_s + run_git("git tag #{tag_name}").to_s end def delete_tag(tag_name) - run(%Q{git push origin --delete #{tag_name}}, max_attempts: 3).to_s + run_git(%Q{git push origin --delete #{tag_name}}, max_attempts: 3).to_s end def commit(message) - run(%Q{git commit -m "#{message}"}, max_attempts: 3).to_s + run_git(%Q{git commit -m "#{message}"}, max_attempts: 3).to_s end def commit_with_gpg(message) - run(%Q{git config user.signingkey #{@gpg_key_id} && git config gpg.program $(command -v gpg) && git commit -S -m "#{message}"}).to_s + run_git(%Q{git config user.signingkey #{@gpg_key_id} && git config gpg.program $(command -v gpg) && git commit -S -m "#{message}"}).to_s end def current_branch - run("git rev-parse --abbrev-ref HEAD").to_s + run_git("git rev-parse --abbrev-ref HEAD").to_s end - def push_changes(branch = 'master') - run("git push #{uri} #{branch}", max_attempts: 3).to_s + def push_changes(branch = 'master', push_options: nil) + cmd = ['git push'] + cmd << push_options_hash_to_string(push_options) + cmd << uri + cmd << branch + run_git(cmd.compact.join(' '), max_attempts: 3).to_s end def push_all_branches - run("git push --all").to_s + run_git("git push --all").to_s end def push_tags_and_branches(branches) - run("git push --tags origin #{branches.join(' ')}").to_s + run_git("git push --tags origin #{branches.join(' ')}").to_s end def merge(branch) - run("git merge #{branch}") + run_git("git merge #{branch}") end def init_repository - run("git init") + run_git("git init") end def pull(repository = nil, branch = nil) - run(['git', 'pull', repository, branch].compact.join(' ')) + run_git(['git', 'pull', repository, branch].compact.join(' ')) end def commits - run('git log --oneline').to_s.split("\n") + run_git('git log --oneline').to_s.split("\n") end def use_ssh_key(key) - @private_key_file = Tempfile.new("id_#{SecureRandom.hex(8)}") - File.binwrite(private_key_file, key.private_key) - File.chmod(0700, private_key_file) - - @known_hosts_file = Tempfile.new("known_hosts_#{SecureRandom.hex(8)}") - keyscan_params = ['-H'] - keyscan_params << "-p #{uri.port}" if uri.port - keyscan_params << uri.host - res = run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}") - return res.response unless res.success? + @ssh = Support::SSH.perform do |ssh| + ssh.key = key + ssh.uri = uri + ssh.setup(env: self.env_vars) + ssh + end - self.env_vars << %Q{GIT_SSH_COMMAND="ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path}"} + self.env_vars << %Q{GIT_SSH_COMMAND="ssh -i #{ssh.private_key_file.path} -o UserKnownHostsFile=#{ssh.known_hosts_file.path}"} end def delete_ssh_key return unless ssh_key_set? - private_key_file.close(true) - known_hosts_file.close(true) + ssh.delete end def push_with_git_protocol(version, file_name, file_content, commit_message = 'Initial commit') @@ -184,13 +184,13 @@ module QA def git_protocol=(value) raise ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2" unless %w[0 1 2].include?(value.to_s) - run("git config protocol.version #{value}") + run_git("git config protocol.version #{value}") end def fetch_supported_git_protocol # ls-remote is one command known to respond to Git protocol v2 so we use # it to get output including the version reported via Git tracing - result = run("git ls-remote #{uri}", env: "GIT_TRACE_PACKET=1", max_attempts: 3) + result = run_git("git ls-remote #{uri}", max_attempts: 3, env: [*self.env_vars, "GIT_TRACE_PACKET=1"]) result.response[/git< version (\d+)/, 1] || 'unknown' end @@ -205,21 +205,16 @@ module QA run("cat #{file}").to_s end + def delete_netrc + File.delete(netrc_file_path) if File.exist?(netrc_file_path) + end + private - attr_reader :uri, :username, :password, :known_hosts_file, - :private_key_file, :use_lfs + attr_reader :uri, :username, :password, :ssh, :use_lfs alias_method :use_lfs?, :use_lfs - Result = Struct.new(:command, :exitstatus, :response) do - alias_method :to_s, :response - - def success? - exitstatus == 0 - end - end - def add_credentials? return false if !username || !password return true unless ssh_key_set? @@ -228,7 +223,7 @@ module QA end def ssh_key_set? - !private_key_file.nil? + ssh && !ssh.private_key_file.nil? end def enable_lfs @@ -237,33 +232,11 @@ module QA touch_gitconfig_result = run("touch #{tmp_home_dir}/.gitconfig") return touch_gitconfig_result.response unless touch_gitconfig_result.success? - git_lfs_install_result = run('git lfs install') + git_lfs_install_result = run_git('git lfs install') touch_gitconfig_result.to_s + git_lfs_install_result.to_s end - def run(command_str, env: [], max_attempts: 1) - command = [env_vars, *env, command_str, '2>&1'].compact.join(' ') - result = nil - - repeat_until(max_attempts: max_attempts, raise_on_failure: false) do - Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]" - output, status = Open3.capture2e(command) - output.chomp! - Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]" - - result = Result.new(command, status.exitstatus, output) - - result.success? - end - - unless result.success? - raise RepositoryCommandError, "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}" - end - - result - end - def default_credentials if ::QA::Runtime::User.ldap_user? [Runtime::User.ldap_username, Runtime::User.ldap_password] @@ -293,6 +266,23 @@ module QA @tmp_home_dir ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s) end + def push_options_hash_to_string(opts) + return if opts.nil? + + prefix = "-o merge_request" + opts.each_with_object([]) do |(key, value), options| + if value.is_a?(Array) + value.each do |item| + options << "#{prefix}.#{key}=\"#{item}\"" + end + elsif value == true + options << "#{prefix}.#{key}" + else + options << "#{prefix}.#{key}=\"#{value}\"" + end + end.join(' ') + end + def netrc_file_path @netrc_file_path ||= File.join(tmp_home_dir, '.netrc') end @@ -304,6 +294,10 @@ module QA def netrc_already_contains_content? read_netrc_content.grep(/^#{Regexp.escape(netrc_content)}$/).any? end + + def run_git(command_str, env: self.env_vars, max_attempts: 1) + run(command_str, env: env, max_attempts: max_attempts, log_prefix: 'Git: ') + end end end end diff --git a/qa/qa/page/component/design_management.rb b/qa/qa/page/component/design_management.rb index a8a24bd3949..fafbda58b07 100644 --- a/qa/qa/page/component/design_management.rb +++ b/qa/qa/page/component/design_management.rb @@ -30,6 +30,17 @@ module QA view 'app/assets/javascripts/design_management/components/list/item.vue' do element :design_file_name element :design_image + element :design_status_icon + end + + view 'app/assets/javascripts/design_management/pages/index.vue' do + element :archive_button + element :design_checkbox + element :design_dropzone_content + end + + view 'app/assets/javascripts/design_management/components/delete_button.vue' do + element :confirm_archiving_button end end end @@ -52,12 +63,14 @@ module QA # It accepts a `class:` option, but that only works for class attributes # It doesn't work as a CSS selector. # So instead we use the name attribute as a locator - page.attach_file("design_file", design_file_path, make_visible: { display: 'block' }) + within_element(:design_dropzone_content) do + page.attach_file("design_file", design_file_path, make_visible: { display: 'block' }) + end filename = ::File.basename(design_file_path) found = wait_until(reload: false, sleep_interval: 1) do - image = find_element(:design_image) + image = find_element(:design_image, filename: filename) has_element?(:design_file_name, text: filename) && image["complete"] && @@ -67,15 +80,41 @@ module QA raise ElementNotFound, %Q(Attempted to attach design "#{filename}" but it did not appear) unless found end + def update_design(filename) + filepath = ::File.join('qa', 'fixtures', 'designs', 'update', filename) + add_design(filepath) + end + def click_design(filename) click_element(:design_file_name, text: filename) end + def select_design(filename) + click_element(:design_checkbox, design: filename) + end + + def archive_selected_designs + click_element(:archive_button) + click_element(:confirm_archiving_button) + end + def has_annotation?(note) within_element_by_index(:design_discussion_content, 0) do has_element?(:note_content, text: note) end end + + def has_design?(filename) + has_element?(:design_file_name, text: filename) + end + + def has_created_icon? + has_element?(:design_status_icon, status: 'file-addition-solid') + end + + def has_modified_icon? + has_element?(:design_status_icon, status: 'file-modified-solid') + end end end end diff --git a/qa/qa/page/component/issuable/sidebar.rb b/qa/qa/page/component/issuable/sidebar.rb index 4e94049efe7..82347ee209a 100644 --- a/qa/qa/page/component/issuable/sidebar.rb +++ b/qa/qa/page/component/issuable/sidebar.rb @@ -18,16 +18,29 @@ module QA element :more_assignees_link end - base.view 'app/helpers/dropdowns_helper.rb' do + base.view 'app/assets/javascripts/sidebar/components/labels/sidebar_labels.vue' do + element :labels_block + end + + base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_value.vue' do + element :selected_label_content + end + + base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue' do + element :labels_dropdown_content + end + + base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue' do + element :labels_edit_button + end + + base.view 'app/assets/javascripts/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view.vue' do element :dropdown_input_field end base.view 'app/views/shared/issuable/_sidebar.html.haml' do element :assignee_block - element :dropdown_menu_labels - element :edit_labels_link element :edit_milestone_link - element :labels_block element :milestone_block element :milestone_link end @@ -64,7 +77,7 @@ module QA def has_label?(label) within_element(:labels_block) do - !!has_element?(:label, label_name: label) + !!has_element?(:selected_label_content, label_name: label) end end @@ -80,23 +93,25 @@ module QA def select_labels_and_refresh(labels) Support::Retrier.retry_until do - click_element(:edit_labels_link) - has_element?(:dropdown_menu_labels, text: labels.first) + click_element(:labels_edit_button) + has_element?(:labels_dropdown_content, text: labels.first) end labels.each do |label| - within_element(:dropdown_menu_labels, text: label) do + within_element(:labels_dropdown_content) do send_keys_to_element(:dropdown_input_field, [label, :enter]) end end - click_element(:edit_labels_link) + click_element(:labels_edit_button) labels.each do |label| has_element?(:labels_block, text: label, wait: 0) end refresh + + wait_for_requests end def toggle_more_assignees_link diff --git a/qa/qa/page/component/new_snippet.rb b/qa/qa/page/component/new_snippet.rb index 3e5ae29177a..741a3feb73b 100644 --- a/qa/qa/page/component/new_snippet.rb +++ b/qa/qa/page/component/new_snippet.rb @@ -21,18 +21,11 @@ module QA base.view 'app/assets/javascripts/snippets/components/snippet_blob_edit.vue' do element :file_name_field + element :file_holder_container end - base.view 'app/views/shared/form_elements/_description.html.haml' do - element :issuable_form_description - end - - base.view 'app/views/shared/snippets/_form.html.haml' do - element :snippet_description_field - element :description_placeholder - element :snippet_title_field - element :file_name_field - element :submit_button + base.view 'app/assets/javascripts/snippets/components/snippet_blob_actions_edit.vue' do + element :add_file_button end base.view 'app/views/shared/_zen.html.haml' do @@ -54,12 +47,28 @@ module QA choose visibility end - def fill_file_name(name) - fill_element :file_name_field, name + def fill_file_name(name, file_number = nil) + if file_number + within_element_by_index(:file_holder_container, file_number - 1) do + fill_element(:file_name_field, name) + end + else + fill_element(:file_name_field, name) + end + end + + def fill_file_content(content, file_number = nil) + if file_number + within_element_by_index(:file_holder_container, file_number - 1) do + text_area.set(content) + end + else + text_area.set content + end end - def fill_file_content(content) - text_area.set content + def click_add_file + click_element(:add_file_button) end def click_create_snippet_button @@ -70,7 +79,7 @@ module QA private def text_area - find('#editor textarea', visible: false) + find('.monaco-editor textarea', visible: false) end end end diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb index 0e9cdd49519..e6defd2ec0c 100644 --- a/qa/qa/page/component/note.rb +++ b/qa/qa/page/component/note.rb @@ -9,9 +9,18 @@ module QA def self.included(base) super + base.view 'app/assets/javascripts/diffs/components/diff_file_header.vue' do + element :toggle_comments_button + end + + base.view 'app/assets/javascripts/notes/components/discussion_actions.vue' do + element :discussion_reply_tab + element :resolve_discussion_button + end + base.view 'app/assets/javascripts/notes/components/comment_form.vue' do element :note_dropdown - element :discussion_option + element :discussion_menu_item end base.view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do @@ -23,39 +32,32 @@ module QA end base.view 'app/assets/javascripts/notes/components/note_form.vue' do - element :reply_input + element :reply_field element :reply_comment_button end - base.view 'app/assets/javascripts/notes/components/discussion_actions.vue' do - element :discussion_reply_tab - element :resolve_discussion_button - end - base.view 'app/assets/javascripts/notes/components/toggle_replies_widget.vue' do - element :expand_replies - element :collapse_replies + element :expand_replies_button + element :collapse_replies_button end - base.view 'app/assets/javascripts/diffs/components/diff_file_header.vue' do - element :toggle_comments_button + base.view 'app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue' do + element :skeleton_note_placeholder end end - def start_discussion(text) - fill_element :comment_input, text - click_element :note_dropdown - click_element :discussion_option - click_element :comment_button + def collapse_replies + click_element :collapse_replies_button end - def toggle_comments(position) - all_elements(:toggle_comments_button, minimum: position)[position - 1].click + def edit_comment(text) + click_element :note_edit_button + fill_element :reply_field, text + click_element :reply_comment_button end - def type_reply_to_discussion(position, reply_text) - all_elements(:discussion_reply_tab, minimum: position)[position - 1].click - fill_element :reply_input, reply_text + def expand_replies + click_element :expand_replies_button end def reply_to_discussion(position, reply_text) @@ -69,18 +71,24 @@ module QA end end - def collapse_replies - click_element :collapse_replies + def start_discussion(text) + fill_element :comment_field, text + click_element :note_dropdown + click_element :discussion_menu_item + click_element :comment_button end - def expand_replies - click_element :expand_replies + def toggle_comments(position) + all_elements(:toggle_comments_button, minimum: position)[position - 1].click end - def edit_comment(text) - click_element :note_edit_button - fill_element :reply_input, text - click_element :reply_comment_button + def type_reply_to_discussion(position, reply_text) + all_elements(:discussion_reply_tab, minimum: position)[position - 1].click + fill_element :reply_field, reply_text + end + + def wait_for_loading + has_no_element?(:skeleton_note_placeholer) end end end diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb index 761bbb17168..87aed0105aa 100644 --- a/qa/qa/page/component/select2.rb +++ b/qa/qa/page/component/select2.rb @@ -43,6 +43,8 @@ module QA end def wait_for_search_to_complete + Support::WaitForRequests.wait_for_requests + has_css?('.select2-active', wait: 1) has_no_css?('.select2-active', wait: 30) end diff --git a/qa/qa/page/component/snippet.rb b/qa/qa/page/component/snippet.rb index 2776b6c078e..9a4b06d8ac7 100644 --- a/qa/qa/page/component/snippet.rb +++ b/qa/qa/page/component/snippet.rb @@ -98,15 +98,39 @@ module QA end end - def has_file_name?(file_name) - within_element(:file_title_content) do - has_text?(file_name) + def has_file_name?(file_name, file_number = nil) + if file_number + within_element_by_index(:file_title_content, file_number - 1) do + has_text?(file_name) + end + else + within_element(:file_title_content) do + has_text?(file_name) + end end end - def has_file_content?(file_content) - within_element(:file_content) do - has_text?(file_content) + def has_file_content?(file_content, file_number = nil) + if file_number + within_element_by_index(:file_content, file_number - 1) do + has_text?(file_content) + end + else + within_element(:file_content) do + has_text?(file_content) + end + end + end + + def has_no_file_content?(file_content, file_number = nil) + if file_number + within_element_by_index(:file_content, file_number - 1) do + has_no_text?(file_content) + end + else + within_element(:file_content) do + has_no_text?(file_content) + end end end @@ -115,7 +139,7 @@ module QA end def click_edit_button - click_element(:snippet_action_button, action: 'Edit') + click_element(:snippet_action_button, Page::Dashboard::Snippet::Edit, action: 'Edit') end def click_delete_button diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb index 7802a680c25..37c0747aea4 100644 --- a/qa/qa/page/dashboard/snippet/edit.rb +++ b/qa/qa/page/dashboard/snippet/edit.rb @@ -5,16 +5,11 @@ module QA module Dashboard module Snippet class Edit < Page::Base - view 'app/views/shared/snippets/_form.html.haml' do - element :submit_button - end - view 'app/assets/javascripts/snippets/components/edit.vue' do - element :submit_button + element :submit_button, required: true end def add_to_file_content(content) - finished_loading? text_area.set content text_area.has_text?(content) # wait for changes to take effect end @@ -30,7 +25,7 @@ module QA private def text_area - find('#editor textarea', visible: false) + find('.monaco-editor textarea', visible: false) end end end diff --git a/qa/qa/page/dashboard/todos.rb b/qa/qa/page/dashboard/todos.rb new file mode 100644 index 00000000000..d8baadcf73d --- /dev/null +++ b/qa/qa/page/dashboard/todos.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +module QA + module Page + module Dashboard + class Todos < Page::Base + include Page::Component::Snippet + + view 'app/views/dashboard/todos/index.html.haml' do + element :todos_list_container, required: true + end + + view 'app/views/dashboard/todos/_todo.html.haml' do + element :todo_item_container + element :todo_action_name_content + element :todo_target_title_content + end + + def has_todo_list? + has_element? :todo_item_container + end + + def has_latest_todo_item_with_content?(action, title) + within_element(:todos_list_container) do + within_element_by_index(:todo_item_container, 0) do + has_element?(:todo_action_name_content, text: action) && has_element?(:todo_target_title_content, text: title) + end + end + end + end + end + end +end diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb index b7808afb209..98bbbc53027 100644 --- a/qa/qa/page/main/sign_up.rb +++ b/qa/qa/page/main/sign_up.rb @@ -5,12 +5,12 @@ module QA module Main class SignUp < Page::Base view 'app/views/devise/shared/_signup_box.html.haml' do - element :new_user_name_field + element :new_user_first_name_field + element :new_user_last_name_field element :new_user_username_field element :new_user_email_field element :new_user_password_field element :new_user_register_button - element :new_user_accept_terms_checkbox end view 'app/views/registrations/welcome.html.haml' do @@ -18,13 +18,12 @@ module QA end def sign_up!(user) - fill_element :new_user_name_field, user.name + fill_element :new_user_first_name_field, user.first_name + fill_element :new_user_last_name_field, user.last_name fill_element :new_user_username_field, user.username fill_element :new_user_email_field, user.email fill_element :new_user_password_field, user.password - check_element :new_user_accept_terms_checkbox if has_element?(:new_user_accept_terms_checkbox) - signed_in = retry_until do click_element :new_user_register_button if has_element?(:new_user_register_button) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index 0b80ba84fa4..164f25389c0 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -44,15 +44,23 @@ module QA element :squash_checkbox end - view 'app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue' do - element :skeleton_note - end - view 'app/views/projects/merge_requests/show.html.haml' do element :notes_tab element :diffs_tab end + view 'app/assets/javascripts/diffs/components/compare_dropdown_layout.vue' do + element :dropdown_content + end + + view 'app/assets/javascripts/diffs/components/compare_versions.vue' do + element :target_version_dropdown + end + + view 'app/assets/javascripts/diffs/components/diff_file_header.vue' do + element :file_name_content + end + view 'app/assets/javascripts/diffs/components/inline_diff_table_row.vue' do element :new_diff_line end @@ -67,15 +75,13 @@ module QA view 'app/assets/javascripts/batch_comments/components/review_bar.vue' do element :review_bar - element :discard_review - element :modal_delete_pending_comments end view 'app/assets/javascripts/notes/components/note_form.vue' do element :unresolve_review_discussion_checkbox element :resolve_review_discussion_checkbox - element :start_review - element :comment_now + element :start_review_button + element :comment_now_button end view 'app/assets/javascripts/batch_comments/components/preview_dropdown.vue' do @@ -83,46 +89,54 @@ module QA end def start_review - click_element :start_review + click_element(:start_review_button) # After clicking the button, wait for it to disappear # before moving on to the next part of the test - has_no_element? :start_review + has_no_element?(:start_review_button) + end + + def click_target_version_dropdown + click_element(:target_version_dropdown) end def comment_now - click_element :comment_now + click_element(:comment_now_button) # After clicking the button, wait for it to disappear # before moving on to the next part of the test - has_no_element? :comment_now + has_no_element?(:comment_now_button) + end + + def version_dropdown_content + find_element(:dropdown_content).text end def submit_pending_reviews - within_element :review_bar do - click_element :review_preview_toggle - click_element :submit_review + within_element(:review_bar) do + click_element(:review_preview_toggle) + click_element(:submit_review) # After clicking the button, wait for it to disappear # before moving on to the next part of the test - has_no_element? :submit_review + has_no_element?(:submit_review) end end def discard_pending_reviews - within_element :review_bar do - click_element :discard_review + within_element(:review_bar) do + click_element(:discard_review) end - click_element :modal_delete_pending_comments + click_element(:modal_delete_pending_comments) end def resolve_review_discussion - scroll_to_element :start_review - check_element :resolve_review_discussion_checkbox + scroll_to_element(:start_review_button) + check_element(:resolve_review_discussion_checkbox) end def unresolve_review_discussion - check_element :unresolve_review_discussion_checkbox + check_element(:unresolve_review_discussion_checkbox) end def add_comment_to_diff(text) @@ -131,7 +145,7 @@ module QA end all_elements(:new_diff_line, minimum: 1).first.hover click_element(:diff_comment) - fill_element(:reply_input, text) + fill_element(:reply_field, text) end def click_discussions_tab @@ -160,6 +174,10 @@ module QA has_no_text?('Fast-forward merge is not possible') end + def has_file?(file_name) + has_element?(:file_name_content, text: file_name) + end + def has_merge_button? refresh @@ -168,7 +186,7 @@ module QA def has_pipeline_status?(text) # Pipelines can be slow, so we wait a bit longer than the usual 10 seconds - has_element?(:merge_request_pipeline_info_content, text: text, wait: 30) + has_element?(:merge_request_pipeline_info_content, text: text, wait: 60) end def has_title?(title) @@ -190,7 +208,7 @@ module QA !find_element(:squash_checkbox).disabled? end - click_element :squash_checkbox + click_element(:squash_checkbox) end def merge! @@ -202,7 +220,7 @@ module QA end def merged? - has_element?(:merged_status_content, text: 'The changes were merged into', wait: 30) + has_element?(:merged_status_content, text: 'The changes were merged into', wait: 60) end # Check if the MR is able to be merged @@ -235,7 +253,7 @@ module QA !find_element(:mr_rebase_button).disabled? end - click_element :mr_rebase_button + click_element(:mr_rebase_button) success = wait_until do has_text?('Fast-forward merge without a merge commit') @@ -251,12 +269,12 @@ module QA end def view_email_patches - click_element :download_dropdown + click_element(:download_dropdown) visit_link_in_element(:download_email_patches) end def view_plain_diff - click_element :download_dropdown + click_element(:download_dropdown) visit_link_in_element(:download_plain_diff) end @@ -266,10 +284,6 @@ module QA end end - def wait_for_loading - has_no_element?(:skeleton_note) - end - def click_open_in_web_ide click_element(:open_in_web_ide_button) wait_for_requests diff --git a/qa/qa/page/profile/accounts/show.rb b/qa/qa/page/profile/accounts/show.rb index cf7f7d80cfa..84a34d1da78 100644 --- a/qa/qa/page/profile/accounts/show.rb +++ b/qa/qa/page/profile/accounts/show.rb @@ -7,6 +7,7 @@ module QA class Show < Page::Base view 'app/views/profiles/accounts/show.html.haml' do element :delete_account_button, required: true + element :enable_2fa_button end view 'app/assets/javascripts/profile/account/components/delete_account_modal.vue' do @@ -14,6 +15,10 @@ module QA element :confirm_delete_account_button end + def click_enable_2fa_button + click_element(:enable_2fa_button) + end + def delete_account(password) click_element(:delete_account_button) diff --git a/qa/qa/page/profile/ssh_keys.rb b/qa/qa/page/profile/ssh_keys.rb index 810877e21ad..8da484003f4 100644 --- a/qa/qa/page/profile/ssh_keys.rb +++ b/qa/qa/page/profile/ssh_keys.rb @@ -11,8 +11,9 @@ module QA element :add_key_button end - view 'app/views/profiles/keys/_key_details.html.haml' do - element :delete_key_button + view 'app/helpers/ssh_keys_helper.rb' do + element :delete_ssh_key_button + element :ssh_key_delete_modal end view 'app/views/profiles/keys/_key_table.html.haml' do @@ -38,8 +39,14 @@ module QA def remove_key(title) click_link(title) - accept_alert do - click_element(:delete_key_button) + click_element(:delete_ssh_key_button) + + # Retrying due to https://gitlab.com/gitlab-org/gitlab/-/issues/255287 + retry_on_exception do + wait_for_animated_element(:ssh_key_delete_modal) + within_element(:ssh_key_delete_modal) do + click_button('Delete') + end end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 826acaa2e0a..a02617def9e 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -12,7 +12,7 @@ module QA view 'app/assets/javascripts/notes/components/comment_form.vue' do element :comment_button - element :comment_input + element :comment_field end view 'app/assets/javascripts/notes/components/discussion_filter.vue' do @@ -43,7 +43,7 @@ module QA end view 'app/assets/javascripts/related_issues/components/related_issuable_input.vue' do - element :add_issue_input + element :add_issue_field end view 'app/assets/javascripts/related_issues/components/related_issues_block.vue' do @@ -57,8 +57,8 @@ module QA def relate_issue(issue) click_element(:related_issues_plus_button) - fill_element(:add_issue_input, issue.web_url) - send_keys_to_element(:add_issue_input, :enter) + fill_element(:add_issue_field, issue.web_url) + send_keys_to_element(:add_issue_field, :enter) end def related_issuable_item @@ -84,7 +84,7 @@ module QA # attachment option should be an absolute path def comment(text, attachment: nil, filter: :all_activities) method("select_#{filter}_filter").call - fill_element :comment_input, "#{text}\n" + fill_element :comment_field, "#{text}\n" unless attachment.nil? QA::Page::Component::Dropzone.new(self, '.new-note') @@ -125,6 +125,8 @@ module QA click_element(:title) click_element :discussion_filter find_element(:filter_options, text: text).click + + wait_for_loading end end end diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 6a657b4ab39..2ecb27e05b2 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -58,6 +58,10 @@ module QA click_element :retry_button end + def has_job_log? + has_element? :job_log_content + end + private def loaded?(wait: 60) @@ -70,3 +74,5 @@ module QA end end end + +QA::Page::Project::Job::Show.prepend_if_ee('QA::EE::Page::Project::Job::Show') diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index f6c015f64ea..7e296528795 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -23,11 +23,7 @@ module QA element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern end - view 'app/views/projects/_import_project_pane.html.haml' do - element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern - end - - view 'app/views/projects/project_templates/_built_in_templates.html.haml' do + view 'app/views/projects/project_templates/_template.html.haml' do element :use_template_button element :template_option_row end diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb index 0c92f9a9f28..114e3ddd46a 100644 --- a/qa/qa/page/project/operations/kubernetes/index.rb +++ b/qa/qa/page/project/operations/kubernetes/index.rb @@ -7,11 +7,11 @@ module QA module Kubernetes class Index < Page::Base view 'app/views/clusters/clusters/_empty_state.html.haml' do - element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern + element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Integrate with a cluster certificate')" # rubocop:disable QA/ElementWithPattern end def add_kubernetes_cluster - click_on 'Add Kubernetes cluster' + click_on 'Connect cluster with certificate' end def has_cluster?(cluster) diff --git a/qa/qa/page/project/packages/index.rb b/qa/qa/page/project/packages/index.rb index 6d55d1d04b6..396d3373b8a 100644 --- a/qa/qa/page/project/packages/index.rb +++ b/qa/qa/page/project/packages/index.rb @@ -26,3 +26,5 @@ module QA end end end + +QA::Page::Project::Packages::Index.prepend_if_ee('QA::EE::Page::Project::Packages::Index') diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb index aa2ef2f058f..aff2378330a 100644 --- a/qa/qa/page/project/pipeline/index.rb +++ b/qa/qa/page/project/pipeline/index.rb @@ -14,6 +14,10 @@ module QA element :pipeline_retry_button end + view 'app/assets/javascripts/pipelines/components/pipelines_list/nav_controls.vue' do + element :run_pipeline_button + end + def click_on_latest_pipeline all_elements(:pipeline_url_link, minimum: 1, wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).first.click end @@ -40,8 +44,18 @@ module QA wait_for_latest_pipeline_success end end + + def has_pipeline? + has_element? :pipeline_url_link + end + + def click_run_pipeline_button + click_element :run_pipeline_button, Page::Project::Pipeline::New + end end end end end end + +QA::Page::Project::Pipeline::Index.prepend_if_ee('QA::EE::Page::Project::Pipeline::Index') diff --git a/qa/qa/page/project/pipeline/new.rb b/qa/qa/page/project/pipeline/new.rb new file mode 100644 index 00000000000..644a21b46e9 --- /dev/null +++ b/qa/qa/page/project/pipeline/new.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Pipeline + class New < QA::Page::Base + view 'app/assets/javascripts/pipeline_new/components/pipeline_new_form.vue' do + element :run_pipeline_button, required: true + end + + def click_run_pipeline_button + click_element :run_pipeline_button + end + end + end + end + end +end diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index 57ab7fb4480..0fb5238a308 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -8,7 +8,7 @@ module QA include Component::CiBadgeLink view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do - element :pipeline_header, /header class.*ci-header-container.*/ # rubocop:disable QA/ElementWithPattern + element :pipeline_header end view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do @@ -16,8 +16,9 @@ module QA end view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do - element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern + element :job_item_container element :job_link + element :action_button end view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do @@ -34,16 +35,18 @@ module QA end def running?(wait: 0) - within('.ci-header-container') do + within_element(:pipeline_header) do page.has_content?('running', wait: wait) end end def has_build?(name, status: :success, wait: nil) - within('.pipeline-graph') do - within('.ci-job-component', text: name) do + if status + within_element(:job_item_container, text: name) do has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact) end + else + has_element?(:job_item_container, text: name) end end @@ -78,6 +81,12 @@ module QA def click_on_first_job first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click end + + def click_job_action(job_name) + within_element(:job_item_container, text: job_name) do + click_element(:action_button) + end + end end end end diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index aef9800e876..f2ced668a60 100644 --- a/qa/qa/page/project/settings/ci_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -26,6 +26,10 @@ module QA within_element(:ci_variable_key_field) { find('input').set key } fill_element :ci_variable_value_field, value click_ci_variable_save_button + + wait_until(reload: false) do + within_element(:ci_variable_table_content) { has_element?(:edit_ci_variable_button) } + end end def click_add_variable diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index c607b35005e..d81be2803bd 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -103,6 +103,8 @@ module QA end def click_commit(commit_msg) + wait_for_requests + within_element(:file_tree_table) do click_on commit_msg end diff --git a/qa/qa/page/project/snippet/index.rb b/qa/qa/page/project/snippet/index.rb new file mode 100644 index 00000000000..a221abc4196 --- /dev/null +++ b/qa/qa/page/project/snippet/index.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Snippet + class Index < Page::Base + include Page::Component::Snippet + + view 'app/views/shared/snippets/_snippet.html.haml' do + element :snippet_link + end + + def has_project_snippet?(title) + has_element?(:snippet_link, snippet_title: title) + end + + def click_snippet_link(title) + within_element(:snippet_link, text: title) do + click_link(title) + end + end + end + end + end + end +end + +QA::Page::Project::Snippet::Index.prepend_if_ee('QA::EE::Page::Project::Snippet::Index') diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 56c8d343cf5..fc33c753230 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -73,6 +73,10 @@ module QA element :project_path_content end + view 'app/assets/javascripts/ide/components/commit_sidebar/message_field.vue' do + element :ide_commit_message_field + end + def has_file?(file_name) within_element(:file_list) do page.has_content? file_name @@ -83,6 +87,10 @@ module QA has_element?(:project_path_content, project_path: project_path) end + def go_to_project + click_element(:project_path_content, Page::Project::Show) + end + def create_new_file_from_template(file_name, template) click_element(:new_file, Page::Component::WebIDE::Modal::CreateNewFile) @@ -115,7 +123,7 @@ module QA find_element(:commit_sha_content).text end - def commit_changes(open_merge_request: false) + def commit_changes(commit_message = nil, open_merge_request: false) # Clicking :begin_commit_button switches from the # edit to the commit view click_element(:begin_commit_button) @@ -133,6 +141,10 @@ module QA has_element?(:commit_button) end + if commit_message + fill_element(:ide_commit_message_field, commit_message) + end + if open_merge_request click_element(:commit_button, Page::MergeRequest::New) else diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb index cdd18e420d1..61b0d202a76 100644 --- a/qa/qa/page/project/wiki/show.rb +++ b/qa/qa/page/project/wiki/show.rb @@ -11,12 +11,12 @@ module QA view 'app/views/shared/wikis/show.html.haml' do element :wiki_page_title element :wiki_page_content + element :edit_page_button end view 'app/views/shared/wikis/_main_links.html.haml' do element :new_page_button element :page_history_button - element :edit_page_button end view 'app/views/shared/empty_states/_wikis.html.haml' do diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index d0cbab70983..034feb4e90f 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -96,15 +96,38 @@ module QA end def api_post - response = post( - Runtime::API::Request.new(api_client, api_post_path).url, - api_post_body) + if api_post_path == "/graphql" + graphql_response = post( + Runtime::API::Request.new(api_client, api_post_path).url, + query: api_post_body) - unless response.code == HTTP_STATUS_CREATED - raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`." + flattened_response = flatten_hash(parse_body(graphql_response)) + + unless graphql_response.code == HTTP_STATUS_OK && flattened_response[:errors].empty? + raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{graphql_response.code}) with `#{graphql_response}`." + end + + flattened_response[:web_url] = flattened_response.delete(:webUrl) + flattened_response[:id] = flattened_response.fetch(:id).split('/')[-1] + + process_api_response(flattened_response) + else + response = post( + Runtime::API::Request.new(api_client, api_post_path).url, + api_post_body) + + unless response.code == HTTP_STATUS_CREATED + raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`." + end + + process_api_response(parse_body(response)) end + end - process_api_response(parse_body(response)) + def flatten_hash(param) + param.each_pair.reduce({}) do |a, (k, v)| + v.is_a?(Hash) ? a.merge(flatten_hash(v)) : a.merge(k.to_sym => v) + end end def api_delete diff --git a/qa/qa/resource/design.rb b/qa/qa/resource/design.rb new file mode 100644 index 00000000000..182985f2d9f --- /dev/null +++ b/qa/qa/resource/design.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module QA + module Resource + class Design < Base + attr_reader :id + attr_accessor :filename + + attribute :issue do + Issue.fabricate_via_api! + end + + def initialize + @update = false + @filename = 'banana_sample.gif' + end + + # TODO This will be replaced as soon as file uploads over GraphQL are implemented + def fabricate! + issue.visit! + + Page::Project::Issue::Show.perform do |issue| + issue.add_design(filepath) + end + end + + private + + def filepath + ::File.absolute_path(::File.join('qa', 'fixtures', 'designs', @filename)) + end + end + end +end diff --git a/qa/qa/resource/events/base.rb b/qa/qa/resource/events/base.rb index 91ec0e59e37..4c5f54825b3 100644 --- a/qa/qa/resource/events/base.rb +++ b/qa/qa/resource/events/base.rb @@ -9,9 +9,12 @@ module QA EventNotFoundError = Class.new(RuntimeError) module Base - def events(action: nil) + def events(action: nil, target_type: nil) + query = [] + query << "action=#{CGI.escape(action)}" if action + query << "target_type=#{CGI.escape(target_type)}" if target_type path = [api_get_events] - path << "?action=#{CGI.escape(action)}" if action + path << "?#{query.join("&")}" unless query.empty? parse_body(api_get_from("#{path.join}")) end diff --git a/qa/qa/resource/events/project.rb b/qa/qa/resource/events/project.rb index 99c78254f42..8c97f66c663 100644 --- a/qa/qa/resource/events/project.rb +++ b/qa/qa/resource/events/project.rb @@ -6,6 +6,13 @@ module QA module Project include Events::Base + def wait_for_merge(title) + QA::Runtime::Logger.debug(%Q[#{self.class.name} - wait_for_merge with title "#{title}"]) + wait_for_event do + events(action: 'accepted', target_type: 'merge_request').any? { |event| event[:target_title] == title } + end + end + def wait_for_push(commit_message) QA::Runtime::Logger.debug(%Q[#{self.class.name} - wait_for_push with commit message "#{commit_message}"]) wait_for_event do diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb index 76c4c71c48d..f573f3e89f0 100644 --- a/qa/qa/resource/file.rb +++ b/qa/qa/resource/file.rb @@ -27,11 +27,14 @@ module QA Page::Project::Show.perform(&:create_first_new_file!) - Page::File::Form.perform do |form| - form.add_name(@name) - form.add_content(@content) - form.add_commit_message(@commit_message) - form.commit_changes + Page::Project::WebIDE::Edit.perform do |ide| + ide.add_file(@name, @content) + ide.commit_changes(@commit_message) + ide.go_to_project + end + + Page::Project::Show.perform do |project| + project.click_file(@name) end end diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb index 6c0f4621dd9..dca8fb6dc6b 100644 --- a/qa/qa/resource/merge_request.rb +++ b/qa/qa/resource/merge_request.rb @@ -1,6 +1,7 @@ # frozen_string_literal: true require 'securerandom' +require 'active_support/core_ext/object/blank' module QA module Resource @@ -17,7 +18,12 @@ module QA :labels, :file_name, :file_content - attr_writer :no_preparation + attr_writer :no_preparation, + :wait_for_merge + + attribute :merge_when_pipeline_succeeds + attribute :merge_status + attribute :state attribute :project do Project.fabricate! do |resource| @@ -58,6 +64,7 @@ module QA @file_content = "File Added" @target_new_branch = true @no_preparation = false + @wait_for_merge = true end def fabricate! @@ -80,10 +87,17 @@ module QA end def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError populate(:target, :source) unless @no_preparation + super end + def api_merge_path + "/projects/#{project.id}/merge_requests/#{id}/merge" + end + def api_get_path "/projects/#{project.id}/merge_requests/#{id}" end @@ -100,6 +114,36 @@ module QA title: @title } end + + def merge_via_api! + Support::Waiter.wait_until(sleep_interval: 1) do + QA::Runtime::Logger.debug("Waiting until merge request with id '#{id}' can be merged") + + reload!.api_resource[:merge_status] == 'can_be_merged' + end + + Support::Retrier.retry_on_exception do + response = put(Runtime::API::Request.new(api_client, api_merge_path).url) + + unless response.code == HTTP_STATUS_OK + raise ResourceUpdateFailedError, "Could not merge. Request returned (#{response.code}): `#{response}`." + end + + result = parse_body(response) + + project.wait_for_merge(result[:title]) if @wait_for_merge + + result + end + end + + private + + def transform_api_resource(api_resource) + raise ResourceNotFoundError if api_resource.blank? + + super(api_resource) + end end end end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 0025ebb2fd5..163c0b40bb5 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -110,6 +110,10 @@ module QA response.any? { |file| file[:path] == file_path } end + def has_branch?(branch) + has_branches?(Array(branch)) + end + def has_branches?(branches) branches.all? do |branch| response = get(Runtime::API::Request.new(api_client, "#{api_repository_branches_path}/#{branch}").url) @@ -140,6 +144,10 @@ module QA "#{api_get_path}/members" end + def api_merge_requests_path + "#{api_get_path}/merge_requests" + end + def api_runners_path "#{api_get_path}/runners" end @@ -223,6 +231,14 @@ module QA result[:import_status] end + def merge_requests + parse_body(get(Runtime::API::Request.new(api_client, api_merge_requests_path).url)) + end + + def merge_request_with_title(title) + merge_requests.find { |mr| mr[:title] == title } + end + def runners(tag_list: nil) response = if tag_list get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url diff --git a/qa/qa/resource/project_snippet.rb b/qa/qa/resource/project_snippet.rb index ce4be6445f1..6fa38baaa91 100644 --- a/qa/qa/resource/project_snippet.rb +++ b/qa/qa/resource/project_snippet.rb @@ -21,6 +21,13 @@ module QA new_snippet.set_visibility(@visibility) new_snippet.fill_file_name(@file_name) new_snippet.fill_file_content(@file_content) + + @files.each.with_index(2) do |file, i| + new_snippet.click_add_file + new_snippet.fill_file_name(file[:name], i) + new_snippet.fill_file_content(file[:content], i) + end + new_snippet.click_create_snippet_button end end diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb index 1e5399fcc59..5266f8b9bea 100644 --- a/qa/qa/resource/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -11,7 +11,7 @@ module QA :branch_name, :new_branch, :output, :repository_http_uri, :repository_ssh_uri, :ssh_key, :user, :use_lfs, :tag_name - attr_writer :remote_branch, :gpg_key_id + attr_writer :remote_branch, :gpg_key_id, :merge_request_push_options def initialize @file_name = "file-#{SecureRandom.hex(8)}.txt" @@ -24,6 +24,7 @@ module QA @use_lfs = false @tag_name = nil @gpg_key_id = nil + @merge_request_push_options = nil end def remote_branch @@ -95,7 +96,7 @@ module QA end @output += commit_to repository - @output += repository.push_changes("#{branch_name}:#{remote_branch}") + @output += repository.push_changes("#{branch_name}:#{remote_branch}", push_options: @merge_request_push_options) end repository.delete_ssh_key diff --git a/qa/qa/resource/snippet.rb b/qa/qa/resource/snippet.rb index 39be5e5cb7d..c4ea6447209 100644 --- a/qa/qa/resource/snippet.rb +++ b/qa/qa/resource/snippet.rb @@ -11,6 +11,11 @@ module QA @visibility = 'Public' @file_content = 'The snippet content' @file_name = 'New snippet file name' + @files = [] + end + + def add_files + yield @files end def fabricate! @@ -22,6 +27,12 @@ module QA new_page.set_visibility(@visibility) new_page.fill_file_name(@file_name) new_page.fill_file_content(@file_content) + + @files.each.with_index(2) do |file, i| + new_page.click_add_file + new_page.fill_file_name(file[:name], i) + new_page.fill_file_content(file[:content], i) + end new_page.click_create_snippet_button end end diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index d4e394954ce..fcd0a479fec 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -76,6 +76,15 @@ module QA parse_body(response)[:title].include?(title) end end + + private + + def api_get + with_paginated_response_body(Runtime::API::Request.new(api_client, '/user/keys', per_page: '100').url) do |page| + key = page.find { |key| key[:title] == title } + break process_api_response(key) if key + end + end end end end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index 9768fc154a7..5cd4147e154 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -11,6 +11,8 @@ module QA attribute :id attribute :name + attribute :first_name + attribute :last_name attribute :email def initialize @@ -34,6 +36,14 @@ module QA @name ||= api_resource&.dig(:name) || "QA User #{unique_id}" end + def first_name + name.split(' ').first + end + + def last_name + name.split(' ').drop(1).join(' ') + end + def email @email ||= begin api_email = api_resource&.dig(:email) diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb index 724b499d32f..b58be354103 100644 --- a/qa/qa/runtime/api/request.rb +++ b/qa/qa/runtime/api/request.rb @@ -34,7 +34,11 @@ module QA # # Returns the relative path to the requested API resource def request_path(path, version: API_VERSION, **query_string) - full_path = ::File.join('/api', version, path) + full_path = if path == '/graphql' + ::File.join('/api', path) + else + ::File.join('/api', version, path) + end if query_string.any? full_path << (path.include?('?') ? '&' : '?') diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index c254be4800b..ddaf35a2d65 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -24,7 +24,7 @@ module QA SUPPORTED_FEATURES end - def address_matches?(*options) + def context_matches?(*options) return false unless Runtime::Scenario.attributes[:gitlab_address] opts = {} @@ -33,29 +33,38 @@ module QA uri = URI(Runtime::Scenario.gitlab_address) - if options.any? - options.each do |option| - opts[:domain] = 'gitlab' if option == :production - - if option.is_a?(Hash) && !option[:subdomain].nil? - opts.merge!(option) - - opts[:subdomain] = case option[:subdomain] - when Array - "(#{option[:subdomain].join("|")})." - when Regexp - option[:subdomain] - else - "(#{option[:subdomain]})." - end - end + options.each do |option| + opts[:domain] = 'gitlab' if option == :production + + if option.is_a?(Hash) && !option[:pipeline].nil? && !ci_project_name.nil? + return pipeline_matches?(option[:pipeline]) + + elsif option.is_a?(Hash) && !option[:subdomain].nil? + opts.merge!(option) + + opts[:subdomain] = case option[:subdomain] + when Array + "(#{option[:subdomain].join("|")})." + when Regexp + option[:subdomain] + else + "(#{option[:subdomain]})." + end end end uri.host.match?(/^#{opts[:subdomain]}#{opts[:domain]}#{opts[:tld]}$/) end - alias_method :dot_com?, :address_matches? + alias_method :dot_com?, :context_matches? + + def pipeline_matches?(pipeline_to_run_in) + Array(pipeline_to_run_in).any? { |pipeline| pipeline.to_s.casecmp?(pipeline_from_project_name) } + end + + def pipeline_from_project_name + ci_project_name.to_s.start_with?('gitlab-qa') ? 'master' : ci_project_name + end def additional_repository_storage ENV['QA_ADDITIONAL_REPOSITORY_STORAGE'] @@ -81,6 +90,10 @@ module QA ENV['GITLAB_QA_ADMIN_ACCESS_TOKEN'] end + def ci_job_url + ENV['CI_JOB_URL'] + end + def ci_project_name ENV['CI_PROJECT_NAME'] end diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb index 579c2293c51..a48bc216ac2 100644 --- a/qa/qa/runtime/feature.rb +++ b/qa/qa/runtime/feature.rb @@ -1,98 +1,125 @@ # frozen_string_literal: true +require 'active_support/core_ext/object/blank' + module QA module Runtime - module Feature - extend self - extend Support::Api + class Feature + class << self + # Documentation: https://docs.gitlab.com/ee/api/features.html - SetFeatureError = Class.new(RuntimeError) - AuthorizationError = Class.new(RuntimeError) + include Support::Api - def enable(key) - QA::Runtime::Logger.info("Enabling feature: #{key}") - set_feature(key, true) - end + SetFeatureError = Class.new(RuntimeError) + AuthorizationError = Class.new(RuntimeError) + UnknownScopeError = Class.new(RuntimeError) - def disable(key) - QA::Runtime::Logger.info("Disabling feature: #{key}") - set_feature(key, false) - end - - def remove(key) - request = Runtime::API::Request.new(api_client, "/features/#{key}") - response = delete(request.url) - unless response.code == QA::Support::Api::HTTP_STATUS_NO_CONTENT - raise SetFeatureError, "Deleting feature flag #{key} failed with `#{response}`." + def remove(key) + request = Runtime::API::Request.new(api_client, "/features/#{key}") + response = delete(request.url) + unless response.code == QA::Support::Api::HTTP_STATUS_NO_CONTENT + raise SetFeatureError, "Deleting feature flag #{key} failed with `#{response}`." + end end - end - - def enable_and_verify(key) - set_and_verify(key, enable: true) - end - def disable_and_verify(key) - set_and_verify(key, enable: false) - end + def enable(key, **scopes) + set_and_verify(key, enable: true, **scopes) + end - def enabled?(key) - feature = JSON.parse(get_features).find { |flag| flag["name"] == key } - feature && feature["state"] == "on" - end + def disable(key, **scopes) + set_and_verify(key, enable: false, **scopes) + end - def get_features - request = Runtime::API::Request.new(api_client, "/features") - response = get(request.url) - response.body - end + def enabled?(key, **scopes) + feature = JSON.parse(get_features).find { |flag| flag['name'] == key.to_s } + feature && feature['state'] == 'on' || feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], scopes) + end - private + private - def api_client - @api_client ||= begin - if Runtime::Env.admin_personal_access_token - Runtime::API::Client.new(:gitlab, personal_access_token: Runtime::Env.admin_personal_access_token) - else - user = Resource::User.fabricate_via_api! do |user| - user.username = Runtime::User.admin_username - user.password = Runtime::User.admin_password - end + def api_client + @api_client ||= Runtime::API::Client.as_admin + rescue Runtime::API::Client::AuthorizationError => e + raise AuthorizationError, "Administrator access is required to enable/disable feature flags. #{e.message}" + end - unless user.admin? - raise AuthorizationError, "Administrator access is required to enable/disable feature flags. User '#{user.username}' is not an administrator." + def enabled_scope?(gates, scopes) + scopes.each do |key, value| + case key + when :project, :group, :user + actors = gates.filter { |i| i['key'] == 'actors' }.first['value'] + break actors.include?("#{key.to_s.capitalize}:#{value.id}") + when :feature_group + groups = gates.filter { |i| i['key'] == 'groups' }.first['value'] + break groups.include?(value) + else + raise UnknownScopeError, "Unknown scope: #{key}" end - - Runtime::API::Client.new(:gitlab, user: user) end end - end - # Change a feature flag and verify that the change was successful - # Arguments: - # key: The feature flag to set (as a string) - # enable: `true` to enable the flag, `false` to disable it - def set_and_verify(key, enable:) - Support::Retrier.retry_on_exception(sleep_interval: 2) do - enable ? enable(key) : disable(key) + def get_features + request = Runtime::API::Request.new(api_client, '/features') + response = get(request.url) + response.body + end - is_enabled = nil + # Change a feature flag and verify that the change was successful + # Arguments: + # key: The feature flag to set (as a string) + # enable: `true` to enable the flag, `false` to disable it + # scopes: Any scope (user, project, group) to restrict the change to + def set_and_verify(key, enable:, **scopes) + msg = "#{enable ? 'En' : 'Dis'}abling feature: #{key}" + msg += " for scope \"#{scopes_to_s(scopes)}\"" if scopes.present? + QA::Runtime::Logger.info(msg) - QA::Support::Waiter.wait_until(sleep_interval: 1) do - is_enabled = enabled?(key) - is_enabled == enable - end + Support::Retrier.retry_on_exception(sleep_interval: 2) do + set_feature(key, enable, scopes) + + is_enabled = nil + + QA::Support::Waiter.wait_until(sleep_interval: 1) do + is_enabled = enabled?(key, scopes) + is_enabled == enable || !enable && scopes.present? + end + + if is_enabled == enable + QA::Runtime::Logger.info("Successfully #{enable ? 'en' : 'dis'}abled and verified feature flag: #{key}") + else + raise SetFeatureError, "#{key} was not #{enable ? 'en' : 'dis'}abled!" if enable - raise SetFeatureError, "#{key} was not #{enable ? 'enabled' : 'disabled'}!" unless is_enabled == enable + QA::Runtime::Logger.warn("Feature flag scope was removed but the flag is still enabled globally.") + end + end + end - QA::Runtime::Logger.info("Successfully #{enable ? 'enabled' : 'disabled'} and verified feature flag: #{key}") + def set_feature(key, value, **scopes) + scopes[:project] = scopes[:project].full_path if scopes.key?(:project) + scopes[:group] = scopes[:group].full_path if scopes.key?(:group) + scopes[:user] = scopes[:user].username if scopes.key?(:user) + request = Runtime::API::Request.new(api_client, "/features/#{key}") + response = post(request.url, scopes.merge({ value: value })) + unless response.code == QA::Support::Api::HTTP_STATUS_CREATED + raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`." + end end - end - def set_feature(key, value) - request = Runtime::API::Request.new(api_client, "/features/#{key}") - response = post(request.url, { value: value }) - unless response.code == QA::Support::Api::HTTP_STATUS_CREATED - raise SetFeatureError, "Setting feature flag #{key} to #{value} failed with `#{response}`." + def scopes_to_s(**scopes) + key = scopes.each_key.first + s = "#{key}: " + case key + when :project, :group + s += scopes[key].full_path + when :user + s += scopes[key].username + when :feature_group + s += scopes[key] + else + raise UnknownScopeError, "Unknown scope: #{key}" + end + + s end end end diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index fb2019394b4..3a17acbb317 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -387,7 +387,7 @@ module QA end def verify_storage_move_to_praefect(repo_path, virtual_storage) - wait_until_shell_command("docker exec #{@gitlab} bash -c 'tail -n 50 /var/log/gitlab/praefect/current'") do |line| + wait_until_shell_command("docker exec #{@praefect} bash -c 'tail -n 50 /var/log/gitlab/praefect/current'") do |line| log = JSON.parse(line) log['grpc.method'] == 'ReplicateRepository' && log['virtual_storage'] == virtual_storage && log['relative_path'] == repo_path diff --git a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb index aa06947e93b..90f58090ccd 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb @@ -22,7 +22,7 @@ module QA end end - after(:context) do + after(:context, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238187', type: :stale }) do # Leave the cluster in a suitable state for subsequent tests, # if there was a problem during the tests here praefect_manager.reset_primary_to_original diff --git a/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb index 4515e76539b..6654a35915f 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/backend_node_recovery_spec.rb @@ -3,7 +3,7 @@ module QA RSpec.describe 'Create' do context 'Gitaly' do - describe 'Backend node recovery', :orchestrated, :gitaly_cluster, :skip_live_env do + describe 'Backend node recovery', :orchestrated, :gitaly_cluster, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238186', type: :investigating } do let(:praefect_manager) { Service::PraefectManager.new } let(:project) do Resource::Project.fabricate! do |project| @@ -22,7 +22,7 @@ module QA praefect_manager.reset_primary_to_original end - it 'recovers from dataloss', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/238186', type: :investigating }, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/978' do + it 'recovers from dataloss', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/978' do # Create a new project with a commit and wait for it to replicate praefect_manager.wait_for_replication(project.id) diff --git a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb index 758ba582929..e96b9ad9258 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/changing_repository_storage_spec.rb @@ -4,7 +4,6 @@ module QA RSpec.describe 'Create' do describe 'Changing Gitaly repository storage', :requires_admin do praefect_manager = Service::PraefectManager.new - praefect_manager.gitlab = 'gitlab' shared_examples 'repository storage move' do it 'confirms a `finished` status after moving project repository storage' do @@ -28,7 +27,6 @@ module QA context 'when moving from one Gitaly storage to another', :orchestrated, :repository_storage, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/973' do let(:source_storage) { { type: :gitaly, name: 'default' } } let(:destination_storage) { { type: :gitaly, name: QA::Runtime::Env.additional_repository_storage } } - let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'repo-storage-move-status' @@ -37,6 +35,10 @@ module QA end end + before do + praefect_manager.gitlab = 'gitlab' + end + it_behaves_like 'repository storage move' end @@ -46,7 +48,6 @@ module QA context 'when moving from Gitaly to Gitaly Cluster', :requires_praefect, testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/974' do let(:source_storage) { { type: :gitaly, name: QA::Runtime::Env.non_cluster_repository_storage } } let(:destination_storage) { { type: :praefect, name: QA::Runtime::Env.praefect_repository_storage } } - let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'repo-storage-move' @@ -56,6 +57,10 @@ module QA end end + before do + praefect_manager.gitlab = 'gitlab-gitaly-cluster' + end + it_behaves_like 'repository storage move' end end diff --git a/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb index 29f131ac322..c3cb503ed3f 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/distributed_reads_spec.rb @@ -17,12 +17,12 @@ module QA end before do - Runtime::Feature.enable_and_verify('gitaly_distributed_reads') + Runtime::Feature.enable(:gitaly_distributed_reads) praefect_manager.wait_for_replication(project.id) end after do - Runtime::Feature.disable_and_verify('gitaly_distributed_reads') + Runtime::Feature.disable(:gitaly_distributed_reads) end it 'reads from each node', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/979' do diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb new file mode 100644 index 00000000000..82a06780830 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_labels_spec.rb @@ -0,0 +1,64 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Merge request push options' do + # If run locally on GDK, push options need to be enabled on the host with the following command: + # + # git config --global receive.advertisepushoptions true + + branch = "push-options-test-#{SecureRandom.hex(8)}" + title = "MR push options test #{SecureRandom.hex(8)}" + commit_message = 'Add README.md' + + project = Resource::Project.fabricate_via_api! do |project| + project.name = 'merge-request-push-options' + project.initialize_with_readme = true + end + + it 'sets labels', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1032' do + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.commit_message = commit_message + push.branch_name = branch + push.merge_request_push_options = { + create: true, + title: title, + label: %w[one two three] + } + end + + merge_request = project.merge_request_with_title(title) + + expect(merge_request).not_to be_nil, "There was a problem creating the merge request" + expect(merge_request[:labels]).to include('one').and include('two').and include('three') + end + + context 'when labels are set already' do + it 'removes them', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1033' do + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.file_content = "Unlabel test #{SecureRandom.hex(8)}" + push.commit_message = commit_message + push.branch_name = branch + push.new_branch = false + push.merge_request_push_options = { + title: title, + unlabel: %w[one three] + } + end + + merge_request = project.merge_request_with_title(title) + + expect(merge_request).not_to be_nil, "There was a problem creating the merge request" + + aggregate_failures do + expect(merge_request[:labels]).to include('two') + expect(merge_request[:labels]).not_to include('one') + expect(merge_request[:labels]).not_to include('three') + end + end + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb new file mode 100644 index 00000000000..dde4708874d --- /dev/null +++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb @@ -0,0 +1,124 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Merge request push options' do + # If run locally on GDK, push options need to be enabled on the host with the following command: + # + # git config --global receive.advertisepushoptions true + + let(:branch) { "push-options-test-#{SecureRandom.hex(8)}" } + let(:title) { "MR push options test #{SecureRandom.hex(8)}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'merge-request-push-options' + project.initialize_with_readme = true + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = "runner-for-#{project.name}" + runner.tags = ["runner-for-#{project.name}"] + end + end + + after do + runner.remove_via_api! + end + + it 'sets merge when pipeline succeeds', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1037' 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 + no-op: + tags: + - "runner-for-#{project.name}" + script: sleep 999 # Leave the pipeline pending + YAML + } + ] + ) + end + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.branch_name = branch + push.merge_request_push_options = { + create: true, + merge_when_pipeline_succeeds: true, + title: title + } + end + + merge_request = project.merge_request_with_title(title) + + expect(merge_request).not_to be_nil, "There was a problem creating the merge request" + + merge_request = Resource::MergeRequest.fabricate_via_api! do |mr| + mr.project = project + mr.id = merge_request[:iid] + end + + expect(merge_request.state).to eq('opened') + expect(merge_request.merge_status).to eq('checking') + expect(merge_request.merge_when_pipeline_succeeds).to be true + end + + it 'merges when pipeline succeeds', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1036' 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 + no-op: + tags: + - "runner-for-#{project.name}" + script: echo 'OK' + YAML + } + ] + ) + end + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.branch_name = branch + push.merge_request_push_options = { + create: true, + merge_when_pipeline_succeeds: true, + title: title + } + end + + merge_request = project.merge_request_with_title(title) + + expect(merge_request).not_to be_nil, "There was a problem creating the merge request" + expect(merge_request[:merge_when_pipeline_succeeds]).to be true + + merge_request = Support::Waiter.wait_until(sleep_interval: 5) do + mr = Resource::MergeRequest.fabricate_via_api! do |mr| + mr.project = project + mr.id = merge_request[:iid] + end + + next unless mr.state == 'merged' + + mr + end + + expect(merge_request.state).to eq('merged') + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb new file mode 100644 index 00000000000..d6bd668fa8a --- /dev/null +++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_remove_source_branch_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Merge request push options' do + # If run locally on GDK, push options need to be enabled on the host with the following command: + # + # git config --global receive.advertisepushoptions true + + let(:branch) { "push-options-test-#{SecureRandom.hex(8)}" } + let(:title) { "MR push options test #{SecureRandom.hex(8)}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'merge-request-push-options' + project.initialize_with_readme = true + end + end + + it 'removes the source branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1035' do + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.branch_name = branch + push.merge_request_push_options = { + create: true, + remove_source_branch: true, + title: title + } + end + + merge_request = project.merge_request_with_title(title) + + expect(merge_request).not_to be_nil, "There was a problem creating the merge request" + + merge_request = Resource::MergeRequest.fabricate_via_api! do |mr| + mr.project = project + mr.id = merge_request[:iid] + end.merge_via_api! + + expect(merge_request[:state]).to eq('merged') + expect(project).not_to have_branch(branch) + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb new file mode 100644 index 00000000000..6072fd8c1a2 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_target_branch_spec.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Merge request push options' do + # If run locally on GDK, push options need to be enabled on the host with the following command: + # + # git config --global receive.advertisepushoptions true + + let(:title) { "MR push options test #{SecureRandom.hex(8)}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'merge-request-push-options' + project.initialize_with_readme = true + end + end + + it 'sets a target branch', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1034' do + target_branch = "push-options-test-target-#{SecureRandom.hex(8)}" + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.branch_name = target_branch + push.file_content = "Target branch test target branch #{SecureRandom.hex(8)}" + end + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.branch_name = "push-options-test-#{SecureRandom.hex(8)}" + push.file_content = "Target branch test source branch #{SecureRandom.hex(8)}" + push.merge_request_push_options = { + create: true, + title: title, + target: target_branch + } + end + + merge_request = project.merge_request_with_title(title) + + expect(merge_request).not_to be_nil, "There was a problem creating the merge request" + expect(merge_request[:target_branch]).to eq(target_branch) + + merge_request = Resource::MergeRequest.fabricate_via_api! do |mr| + mr.project = project + mr.id = merge_request[:iid] + end.merge_via_api! + + expect(merge_request[:state]).to eq('merged') + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb new file mode 100644 index 00000000000..f49a8a229dc --- /dev/null +++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_title_description_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Merge request push options' do + # If run locally on GDK, push options need to be enabled on the host with the following command: + # + # git config --global receive.advertisepushoptions true + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'merge-request-push-options' + project.initialize_with_readme = true + end + end + + it 'sets title and description', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1038' do + description = "This is a test of MR push options" + title = "MR push options test #{SecureRandom.hex(8)}" + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.branch_name = "push-options-test-#{SecureRandom.hex(8)}" + push.merge_request_push_options = { + create: true, + title: title, + description: description + } + end + + merge_request = project.merge_request_with_title(title) + + expect(merge_request).not_to be_nil, "There was a problem creating the merge request" + + aggregate_failures do + expect(merge_request[:title]).to eq(title) + expect(merge_request[:description]).to eq(description) + end + + merge_request = Resource::MergeRequest.fabricate_via_api! do |mr| + mr.project = project + mr.id = merge_request[:iid] + end.merge_via_api! + + expect(merge_request[:state]).to eq('merged') + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb b/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb index af155b22618..f86bbee05c2 100644 --- a/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/default_branch_name_setting_spec.rb @@ -13,7 +13,7 @@ module QA Runtime::ApplicationSettings.restore_application_settings(:default_branch_name) end - it 'sets the default branch name for a new project' do + it 'sets the default branch name for a new project', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1018' do project = Resource::Project.fabricate_via_api! do |project| project.name = "default-branch-name" project.initialize_with_readme = true @@ -32,7 +32,7 @@ module QA end end - it 'allows a project to be created via the CLI with a different default branch name' do + it 'allows a project to be created via the CLI with a different default branch name', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1019' do project_name = "default-branch-name-via-cli-#{SecureRandom.hex(8)}" group = Resource::Group.fabricate_via_api! diff --git a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb index 4bd99b4820e..548933d2cde 100644 --- a/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/project_archive_compare_spec.rb @@ -28,6 +28,13 @@ module QA end end + after do + # Delete the .netrc file created during this test so that subsequent tests don't try to use the logins + Git::Repository.perform do |repository| + repository.delete_netrc + end + end + it 'download archives of each user project then check they are different', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/427' do archive_checksums = {} diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb new file mode 100644 index 00000000000..e81ebd5fa9d --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_ssh_recovery_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module QA + context 'Manage', :requires_admin, :skip_live_env do + describe '2FA' do + let!(:user) { Resource::User.fabricate_via_api! } + let!(:user_api_client) { Runtime::API::Client.new(:gitlab, user: user) } + let(:address) { QA::Runtime::Scenario.gitlab_address } + let(:uri) { URI.parse(address) } + let(:ssh_port) { uri.port == 80 ? '' : '2222' } + let!(:ssh_key) do + Resource::SSHKey.fabricate_via_api! do |resource| + resource.title = "key for ssh tests #{Time.now.to_f}" + resource.api_client = user_api_client + end + end + + before do + enable_2fa_for_user(user) + end + + it 'allows 2FA code recovery via ssh' do + recovery_code = Support::SSH.perform do |ssh| + ssh.key = ssh_key + ssh.uri = address.gsub(uri.port.to_s, ssh_port) + ssh.setup + output = ssh.reset_2fa_codes + output.scan(/([A-Za-z0-9]{16})\n/).flatten.first + end + + Flow::Login.sign_in(as: user, skip_page_validation: true) + Page::Main::TwoFactorAuth.perform do |two_fa_auth| + two_fa_auth.set_2fa_code(recovery_code) + two_fa_auth.click_verify_code_button + end + + expect(Page::Main::Menu.perform(&:signed_in?)).to be_truthy + + Page::Main::Menu.perform(&:sign_out) + Flow::Login.sign_in(as: user, skip_page_validation: true) + Page::Main::TwoFactorAuth.perform do |two_fa_auth| + two_fa_auth.set_2fa_code(recovery_code) + two_fa_auth.click_verify_code_button + end + + expect(page).to have_text('Invalid two-factor code') + end + + def enable_2fa_for_user(user) + Flow::Login.while_signed_in(as: user) do + Page::Main::Menu.perform(&:click_settings_link) + Page::Profile::Menu.perform(&:click_account) + Page::Profile::Accounts::Show.perform(&:click_enable_2fa_button) + + Page::Profile::TwoFactorAuth.perform do |two_fa_auth| + otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content) + two_fa_auth.set_pin_code(otp.fresh_otp) + two_fa_auth.click_register_2fa_app_button + two_fa_auth.click_proceed_button + end + end + end + end + end +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 bd0d28c86be..e514507fcb6 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 @@ -18,7 +18,7 @@ module QA QA::Resource::Group.fabricate_via_api! do |group| group.sandbox = sandbox_group group.api_client = owner_api_client - group.name = 'group-with-2fa' + group.path = "group-with-2fa-#{SecureRandom.hex(8)}" end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index a10329d5936..863c394a9f9 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -41,7 +41,7 @@ module QA context 'when using attachments in comments', :object_storage do let(:gif_file_name) { 'banana_sample.gif' } let(:file_to_attach) do - File.absolute_path(File.join('spec', 'fixtures', gif_file_name)) + File.absolute_path(File.join('qa', 'fixtures', 'designs', gif_file_name)) end before do 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 b011978dce6..c908b1c46a1 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Plan' do + RSpec.describe 'Plan', :reliable do describe 'Milestones' do include Support::Dates diff --git a/qa/qa/specs/features/browser_ui/2_plan/milestone/create_group_milestone_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/milestone/create_group_milestone_spec.rb index 5f7e28190b2..564cfbb8399 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/milestone/create_group_milestone_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/milestone/create_group_milestone_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Plan' do + RSpec.describe 'Plan', :reliable do describe 'Group milestone' do include Support::Dates diff --git a/qa/qa/specs/features/browser_ui/2_plan/milestone/create_project_milestone_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/milestone/create_project_milestone_spec.rb index 489691b4d0c..99d547acb26 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/milestone/create_project_milestone_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/milestone/create_project_milestone_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Plan' do + RSpec.describe 'Plan', :reliable do describe 'Project milestone' do include Support::Dates diff --git a/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb index a2190a8cf41..13761244300 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/related_issues/related_issues_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Plan' do + RSpec.describe 'Plan', :reliable do describe 'Related issues' do let(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb index ff2b4fa5364..051e8fcecbe 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb @@ -5,7 +5,7 @@ module QA context 'Design Management' do let(:issue) { Resource::Issue.fabricate_via_api! } let(:design_filename) { 'banana_sample.gif' } - let(:design) { File.absolute_path(File.join('spec', 'fixtures', design_filename)) } + let(:design) { File.absolute_path(File.join('qa', 'fixtures', 'designs', design_filename)) } let(:annotation) { "This design is great!" } before do diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb new file mode 100644 index 00000000000..7090427e5a4 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + context 'Design Management' do + let(:first_design) { Resource::Design.fabricate! } + + let(:second_design) do + Resource::Design.fabricate! do |design| + design.issue = first_design.issue + design.filename = 'values.png' + end + end + + let(:third_design) do + Resource::Design.fabricate! do |design| + design.issue = second_design.issue + design.filename = 'tanuki.jpg' + end + end + + before do + Flow::Login.sign_in + end + + it 'user archives a design', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/274' do + third_design.issue.visit! + + Page::Project::Issue::Show.perform do |issue| + issue.select_design(third_design.filename) + + issue.archive_selected_designs + + expect(issue).not_to have_design(third_design.filename) + expect(issue).to have_design(first_design.filename) + expect(issue).to have_design(second_design.filename) + end + + Page::Project::Issue::Show.perform do |issue| + issue.select_design(second_design.filename) + issue.select_design(first_design.filename) + + issue.archive_selected_designs + + expect(issue).not_to have_design(first_design.filename) + expect(issue).not_to have_design(second_design.filename) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb new file mode 100644 index 00000000000..135063b6644 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + context 'Design Management' do + let(:design) do + Resource::Design.fabricate! do |design| + design.filename = 'tanuki.jpg' + end + end + + before do + Flow::Login.sign_in + end + + it 'user adds a design and modifies it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/273' do + design.issue.visit! + + Page::Project::Issue::Show.perform do |issue| + expect(issue).to have_created_icon + end + + Page::Project::Issue::Show.perform do |issue| + issue.update_design(design.filename) + expect(issue).to have_modified_icon + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_merge_ref_diff_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_merge_ref_diff_spec.rb new file mode 100644 index 00000000000..7844d0d7ccb --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_merge_ref_diff_spec.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create', :requires_admin do + describe 'View merge request merge-ref diff' do + let(:merge_request) do + Resource::MergeRequest.fabricate_via_api! do |merge_request| + merge_request.title = 'This is a merge request' + merge_request.description = '... for viewing merge-ref and merge-base diffs' + merge_request.file_content = 'This exists on the source branch only' + end + end + + let(:new_file_name) { "added_file-#{SecureRandom.hex(8)}.txt" } + + before do + commit_to_branch(merge_request.target_branch, new_file_name) + commit_to_branch(merge_request.source_branch, new_file_name) + + Flow::Login.sign_in + end + + context 'when the feature flag default_merge_ref_for_diffs is enabled' do + before do + Runtime::Feature.enable('default_merge_ref_for_diffs', project: merge_request.project) + + merge_request.visit! + end + + it 'views the merge-ref diff by default' do + Page::MergeRequest::Show.perform do |mr_page| + mr_page.click_diffs_tab + mr_page.click_target_version_dropdown + + expect(mr_page.version_dropdown_content).to include('master (HEAD)') + expect(mr_page.version_dropdown_content).not_to include('master (base)') + expect(mr_page).to have_file(merge_request.file_name) + expect(mr_page).not_to have_file(new_file_name) + end + end + end + + context 'when the feature flag default_merge_ref_for_diffs is disabled' do + before do + Runtime::Feature.disable('default_merge_ref_for_diffs', project: merge_request.project) + + merge_request.visit! + end + + it 'views the merge-base diff by default' do + Page::MergeRequest::Show.perform do |mr_page| + mr_page.click_diffs_tab + mr_page.click_target_version_dropdown + + expect(mr_page.version_dropdown_content).to include('master (HEAD)') + expect(mr_page.version_dropdown_content).to include('master (base)') + expect(mr_page).to have_file(merge_request.file_name) + expect(mr_page).to have_file(new_file_name) + end + end + end + + def commit_to_branch(branch, file) + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = merge_request.project + commit.branch = branch + commit.commit_message = "Add new file on #{branch}" + commit.add_files( + [ + { + file_path: file, + content: "This exists on source and target branches" + } + ] + ) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb index 43f4b080c73..5aa5f0fc0a3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb @@ -18,7 +18,6 @@ module QA file.commit_message = commit_message_for_create end - expect(page).to have_content('The file has been successfully created.') expect(page).to have_content(file_name) expect(page).to have_content(file_content) expect(page).to have_content(commit_message_for_create) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb index 8ac7285d70c..5781bf8a7f0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb @@ -37,8 +37,10 @@ module QA project.wait_for_push_new_branch # Check that the push worked - expect(page).to have_content(file_name) - expect(page).to have_content(file_content) + Page::Project::Show.perform do |project_page| + expect(project_page).to have_file(file_name) + expect(project_page).to have_readme_content(file_content) + end # And check that the correct Git protocol was used expect(git_protocol_reported).to eq(git_protocol) 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 f96b424d233..45afa252305 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 @@ -4,7 +4,6 @@ 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/quality/testcases/-/issues/414' do - Runtime::Feature.enable_and_verify('push_mirror_syncs_lfs') Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) @@ -37,8 +36,10 @@ module QA # Check that the target project has the commit from the source target_project.visit! - expect(page).to have_content('README.md') - expect(page).to have_content('The rendered file could not be displayed because it is stored in LFS') + Page::Project::Show.perform do |project_page| + expect(project_page).to have_file('README.md') + expect(project_page).to have_readme_content('The rendered file could not be displayed because it is stored in LFS') + end end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb index 2ebab7d2a30..222eb3771ad 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_file_size_spec.rb @@ -41,7 +41,7 @@ module QA retry_on_fail do expect { push_new_file('oversize_file_2.bin', wait_for_push: false) } - .to raise_error(QA::Git::Repository::RepositoryCommandError, /remote: fatal: pack exceeds maximum allowed size/) + .to raise_error(QA::Support::Run::CommandError, /remote: fatal: pack exceeds maximum allowed size/) end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index 8b6973e6cea..cf14017b7f1 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -36,8 +36,10 @@ module QA project.visit! - expect(page).to have_content('README.md') - expect(page).to have_content("This is a test project named #{project.name}") + Page::Project::Show.perform do |project_page| + expect(project_page).to have_file('README.md') + expect(project_page).to have_readme_content("This is a test project named #{project.name}") + end end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index d20abd658c6..54d00209cc7 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -35,7 +35,7 @@ module QA roles: Resource::ProtectedBranch::Roles::NO_ONE }) - expect { push_new_file(branch_name) }.to raise_error(QA::Git::Repository::RepositoryCommandError, /remote: GitLab: You are not allowed to push code to protected branches on this project\.([\s\S]+)\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/) + expect { push_new_file(branch_name) }.to raise_error(QA::Support::Run::CommandError, /remote: GitLab: You are not allowed to push code to protected branches on this project\.([\s\S]+)\[remote rejected\] #{branch_name} -> #{branch_name} \(pre-receive hook declined\)/) end end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb index ef3b315506f..a3f6d521766 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 @@ -36,6 +36,10 @@ module QA Flow::Login.sign_in end + after do + ssh_key.remove_via_api! + end + it 'clones, pushes, and pulls a snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/826' do Resource::Repository::Push.fabricate! do |push| push.repository_http_uri = repository_uri_http @@ -87,7 +91,7 @@ module QA repository.init_repository expect { repository.pull(repository_uri_ssh, branch_name) } - .to raise_error(QA::Git::Repository::RepositoryCommandError, /fatal: Could not read from remote repository\./) + .to raise_error(QA::Support::Run::CommandError, /fatal: Could not read from remote repository\./) end end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb index 34f6b464f29..be56b870490 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 @@ -36,6 +36,10 @@ module QA Flow::Login.sign_in end + after do + ssh_key.remove_via_api! + end + it 'clones, pushes, and pulls a project snippet over HTTP, edits via UI', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/833' do Resource::Repository::Push.fabricate! do |push| push.repository_http_uri = repository_uri_http @@ -86,7 +90,7 @@ module QA repository.init_repository expect { repository.pull(repository_uri_ssh, branch_name) } - .to raise_error(QA::Git::Repository::RepositoryCommandError, /fatal: Could not read from remote repository\./) + .to raise_error(QA::Support::Run::CommandError, /fatal: Could not read from remote repository\./) end end 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 new file mode 100644 index 00000000000..50f2f4789fa --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_with_multiple_files_spec.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Multiple file snippet' do + it 'creates a personal snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/842' do + Flow::Login.sign_in + + Page::Main::Menu.perform do |menu| + menu.go_to_more_dropdown_option(:snippets_link) + end + + Resource::Snippet.fabricate_via_browser_ui! do |snippet| + snippet.title = 'Personal snippet with multiple files' + snippet.description = 'Snippet description' + snippet.visibility = 'Public' + snippet.file_name = 'First file name' + snippet.file_content = 'First file content' + + snippet.add_files do |files| + files.append(name: 'Second file name', content: 'Second file content') + files.append(name: 'Third file name', content: 'Third file content') + end + end + + Page::Dashboard::Snippet::Show.perform do |snippet| + expect(snippet).to have_snippet_title('Personal snippet with multiple files') + expect(snippet).to have_snippet_description('Snippet description') + expect(snippet).to have_visibility_type(/public/i) + expect(snippet).to have_file_name('First file name', 1) + expect(snippet).to have_file_content('First file content', 1) + expect(snippet).to have_file_name('Second file name', 2) + expect(snippet).to have_file_content('Second file content', 2) + expect(snippet).to have_file_name('Third file name', 3) + expect(snippet).to have_file_content('Third file content', 3) + end + end + end + end +end 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 0a8f6e13b2e..d80fc4c5b95 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 @@ -21,8 +21,8 @@ module QA expect(snippet).to have_file_name('markdown_file.md') expect(snippet).to have_file_content('Snippet heading') expect(snippet).to have_file_content('Gitlab link') - expect(snippet).not_to have_file_content('###') - expect(snippet).not_to have_file_content('https://gitlab.com/') + expect(snippet).to have_no_file_content('###') + expect(snippet).to have_no_file_content('https://gitlab.com/') end end end 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 new file mode 100644 index 00000000000..7b4ec573f53 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_project_snippet_with_multiple_files_spec.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Multiple file snippet' do + it 'creates a project snippet with multiple files', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1024' do + Flow::Login.sign_in + + Resource::ProjectSnippet.fabricate_via_browser_ui! do |snippet| + snippet.title = 'Project snippet with multiple files' + snippet.description = 'Snippet description' + snippet.visibility = 'Private' + snippet.file_name = '01 file name' + snippet.file_content = '1 file content' + + # Ten is the limit of files you can have under one snippet at the moment + snippet.add_files do |files| + (2..10).each do |i| + files.append(name: file_name(i), content: file_content(i)) + end + end + end + + Page::Dashboard::Snippet::Show.perform do |snippet| + aggregate_failures 'file content verification' do + expect(snippet).to have_snippet_title('Project snippet with multiple files') + expect(snippet).to have_snippet_description('Snippet description') + expect(snippet).to have_visibility_type(/private/i) + + (1..10).each do |i| + expect(snippet).to have_file_name(file_name(i), i) + expect(snippet).to have_file_content(file_content(i), i) + end + end + end + end + + # Currently the files are returned in alphabetical order and not in the order they are created. + # However, it might soon change - see https://gitlab.com/gitlab-org/gitlab/-/issues/250836. + # By using a leading "0" we make sure the test works with either implementation. + def file_name(index) + "#{index.to_s.rjust(2, '0')} file name" + end + + def file_content(index) + "#{index} file content" + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/create_first_file_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/create_first_file_in_web_ide_spec.rb index ea821f8b3e6..f7a2e3081fb 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/create_first_file_in_web_ide_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/create_first_file_in_web_ide_spec.rb @@ -10,7 +10,6 @@ module QA end end - let(:web_ide_url) { current_url + '-/ide/project/' + project.path_with_namespace } let(:file_name) { 'the very first file.txt' } before do @@ -18,10 +17,8 @@ module QA end it "creates the first file in an empty project via Web IDE", testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/847' do - # In the first iteration, the test opens Web IDE by modifying the URL to address past regressions. - # Once the Web IDE button is introduced for empty projects, the test will be modified to go through UI. - # See https://gitlab.com/gitlab-org/gitlab/-/issues/27915 and https://gitlab.com/gitlab-org/gitlab/-/issues/27535. - page.visit(web_ide_url) + project.visit! + Page::Project::Show.perform(&:create_first_new_file!) Page::Project::WebIDE::Edit.perform do |ide| ide.create_first_file(file_name) diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb index e8053600930..4f1d9ac1696 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/project_based_directory_management_spec.rb @@ -4,7 +4,7 @@ module QA RSpec.describe 'Create' do context 'Wiki' do let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! } - let(:new_path) { "a/new/path" } + let(:new_path) { "a/new/path-with-spaces" } before do Flow::Login.sign_in @@ -23,7 +23,9 @@ module QA Page::Project::Wiki::Edit.perform(&:click_save_changes) Page::Project::Wiki::Show.perform do |wiki| - expect(wiki).to have_directory(new_path) + expect(wiki).to have_directory('a') + expect(wiki).to have_directory('new') + expect(wiki).to have_directory('path with spaces') end end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb index cc4d0e1f6b5..ccd4d34a916 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_remove_ci_variable_spec.rb @@ -3,7 +3,7 @@ module QA RSpec.describe 'Verify' do describe 'Add or Remove CI variable via UI', :smoke do - let!(:project) do + let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'project-with-ci-variables' project.description = 'project with CI variables' @@ -12,31 +12,27 @@ module QA before do Flow::Login.sign_in + project.visit! add_ci_variable - open_ci_cd_settings end it 'user adds a CI variable', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/395' do - Page::Project::Settings::CICD.perform do |settings| - settings.expand_ci_variables do |page| - expect(page).to have_text('VARIABLE_KEY') - expect(page).not_to have_text('some_CI_variable') + Page::Project::Settings::CiVariables.perform do |ci_variable| + expect(ci_variable).to have_text('VARIABLE_KEY') + expect(ci_variable).to have_no_text('some_CI_variable') - page.click_reveal_ci_variable_value_button + ci_variable.click_reveal_ci_variable_value_button - expect(page).to have_text('some_CI_variable') - end + expect(ci_variable).to have_text('some_CI_variable') end end it 'user removes a CI variable', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/394' do - Page::Project::Settings::CICD.perform do |settings| - settings.expand_ci_variables do |page| - page.click_edit_ci_variable - page.click_ci_variable_delete_button + Page::Project::Settings::CiVariables.perform do |ci_variable| + ci_variable.click_edit_ci_variable + ci_variable.click_ci_variable_delete_button - expect(page).not_to have_text('VARIABLE_KEY') - end + expect(ci_variable).to have_text('There are no variables yet', wait: 60) end end @@ -50,11 +46,6 @@ module QA ci_variable.masked = false end end - - def open_ci_cd_settings - project.visit! - Page::Project::Menu.perform(&:go_to_ci_cd_settings) - end end end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index 326647b25f7..8de739f1559 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Verify', :docker, :runner do + RSpec.describe 'Verify', :runner do describe 'Pipeline creation and processing' do let(:executor) { "qa-runner-#{Time.now.to_i}" } let(:max_wait) { 30 } 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 new file mode 100644 index 00000000000..153ccafaa20 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_via_web_only_spec.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Verify' do + describe 'Run pipeline', :requires_admin, :skip_live_env do + # [TODO]: Developer to remove :requires_admin and :skip_live_env once FF is removed in https://gitlab.com/gitlab-org/gitlab/-/issues/229632 + + context 'with web only rule' do + let(:feature_flag) { :new_pipeline_form } + let(:job_name) { 'test_job' } + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'web-only-pipeline' + end + end + + let!(:ci_file) 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 + #{job_name}: + tags: + - #{project.name} + script: echo 'OK' + only: + - web + YAML + } + ] + ) + end + end + + before do + Runtime::Feature.enable(feature_flag) # [TODO]: Developer to remove when feature flag is removed + Flow::Login.sign_in + project.visit! + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + end + + after do + Runtime::Feature.disable(feature_flag) # [TODO]: Developer to remove when feature flag is removed + end + + it 'can trigger pipeline', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/946' do + Page::Project::Pipeline::Index.perform do |index| + expect(index).not_to have_pipeline # should not auto trigger pipeline + index.click_run_pipeline_button + end + + Page::Project::Pipeline::New.perform(&:click_run_pipeline_button) + + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to have_job(job_name) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb new file mode 100644 index 00000000000..39d5fbaba6b --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/trigger_child_pipeline_with_manual_spec.rb @@ -0,0 +1,109 @@ +# frozen_string_literal: true + +require 'faker' + +module QA + RSpec.describe 'Verify', :runner, :requires_admin do + # [TODO]: Developer to remove :requires_admin once FF is removed in follow up issue + + describe "Trigger child pipeline with 'when:manual'" do + let(:feature_flag) { :ci_manual_bridges } # [TODO]: Developer to remove when feature flag is removed + let(:executor) { "qa-runner-#{Faker::Alphanumeric.alphanumeric(8)}" } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'project-with-pipeline' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + before do + Runtime::Feature.enable(feature_flag) # [TODO]: Developer to remove when feature flag is removed + Flow::Login.sign_in + add_ci_files + project.visit! + view_the_last_pipeline + end + + after do + Runtime::Feature.disable(feature_flag) # [TODO]: Developer to remove when feature flag is removed + runner.remove_via_api! + end + + it 'can trigger bridge job', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/1049' do + Page::Project::Pipeline::Show.perform do |parent_pipeline| + expect(parent_pipeline).not_to have_child_pipeline + + parent_pipeline.click_job_action('trigger') + Support::Waiter.wait_until { parent_pipeline.has_child_pipeline? } + parent_pipeline.expand_child_pipeline + + expect(parent_pipeline).to have_build('child_build', status: nil) + end + end + + private + + def add_ci_files + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add parent and child pipelines CI files.' + commit.add_files( + [ + child_ci_file, + parent_ci_file + ] + ) + end + end + + def view_the_last_pipeline + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success) + Page::Project::Pipeline::Index.perform(&:click_on_latest_pipeline) + end + + def parent_ci_file + { + file_path: '.gitlab-ci.yml', + content: <<~YAML + build: + stage: build + tags: ["#{executor}"] + script: echo build + + trigger: + stage: test + when: manual + trigger: + include: '.child-pipeline.yml' + + deploy: + stage: deploy + tags: ["#{executor}"] + script: echo deploy + YAML + } + end + + def child_ci_file + { + file_path: '.child-pipeline.yml', + content: <<~YAML + child_build: + stage: build + tags: ["#{executor}"] + script: echo build + YAML + } + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb index a296d60b27c..9ce87f353d0 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Verify', :docker, :runner do + RSpec.describe 'Verify', :runner do describe 'Runner registration' do let(:executor) { "qa-runner-#{Time.now.to_i}" } let!(:runner) do diff --git a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb index f4edaaa84a8..5bfc88e45f2 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Verify', :docker, :runner do + RSpec.describe 'Verify', :runner do describe 'Code coverage statistics' do let(:simplecov) { '\(\d+.\d+\%\) covered' } let(:executor) { "qa-runner-#{Time.now.to_i}" } diff --git a/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb index a617f3b3e29..4ca356c9b65 100644 --- a/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/maven_repository_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :docker, :orchestrated, :packages do + RSpec.describe 'Package', :orchestrated, :packages do describe 'Maven Repository' do include Runtime::Fixtures diff --git a/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb index e97ede35610..817e146adfe 100644 --- a/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/npm_registry_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package', :docker, :orchestrated, :packages do + RSpec.describe 'Package', :orchestrated, :packages do describe 'NPM registry' do include Runtime::Fixtures @@ -21,7 +21,7 @@ module QA end end - it 'publishes an npm package and then deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/944', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/247281', type: :investigating } do + it 'publishes an npm package and then deletes it', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/944' do uri = URI.parse(Runtime::Scenario.gitlab_address) gitlab_host_with_port = "#{uri.host}:#{uri.port}" gitlab_address_with_port = "#{uri.scheme}://#{uri.host}:#{uri.port}" @@ -68,7 +68,7 @@ module QA end Page::Project::Packages::Index.perform do |index| - expect(index).to have_content("Package was removed") + expect(index).to have_content("Package deleted successfully") expect(index).to have_no_package(package_name) end 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 18eb52830a2..abac4f2b91d 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 @@ -3,7 +3,7 @@ require 'digest/sha1' module QA - RSpec.describe 'Release', :docker, :runner do + RSpec.describe 'Release', :runner do describe 'Git clone using a deploy key' do before do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb index 47a1b3b5670..ece45d093a7 100644 --- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Release', :docker, :runner, :reliable do + RSpec.describe 'Release', :runner, :reliable do describe 'Parent-child pipelines dependent relationship' do let!(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb index 9eb81244aa4..38cee0e62ca 100644 --- a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_independent_relationship_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Release', :docker, :runner, :reliable do + RSpec.describe 'Release', :runner, :reliable do describe 'Parent-child pipelines independent relationship' do let!(:project) do Resource::Project.fabricate_via_api! do |project| diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 53a1c8010af..6d31780f196 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -15,7 +15,7 @@ module QA disable_optional_jobs(project) end - describe 'Auto DevOps support', :orchestrated, :kubernetes, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/230927', type: :stale } do + describe 'Auto DevOps support', :orchestrated, :kubernetes, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/251090', type: :stale } do context 'when rbac is enabled' do let(:cluster) { Service::KubernetesCluster.new.create! } @@ -24,6 +24,8 @@ module QA end it 'runs auto devops', testcase: 'https://gitlab.com/gitlab-org/quality/testcases/-/issues/702' do + skip('Test requires tunnel: see https://gitlab.com/gitlab-org/gitlab/-/issues/251090') + Flow::Login.sign_in # Set an application secret CI variable (prefixed with K8S_SECRET_) diff --git a/qa/qa/specs/features/sanity/framework_spec.rb b/qa/qa/specs/features/sanity/framework_spec.rb index 611c6c7b1ff..feec56478c0 100644 --- a/qa/qa/specs/features/sanity/framework_spec.rb +++ b/qa/qa/specs/features/sanity/framework_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Framework sanity checks', :orchestrated, :framework do + RSpec.describe 'Framework sanity checks', :orchestrated, :framework do describe 'Passing orchestrated example' do it 'succeeds' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb index 6a3becf0ee5..d365819057e 100644 --- a/qa/qa/specs/helpers/quarantine.rb +++ b/qa/qa/specs/helpers/quarantine.rb @@ -20,7 +20,7 @@ module QA Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example) if example.metadata.key?(:only) - skip('Test is not compatible with this environment') unless Runtime::Env.address_matches?(example.metadata[:only]) + skip('Test is not compatible with this environment or pipeline') unless Runtime::Env.context_matches?(example.metadata[:only]) end end end @@ -55,7 +55,7 @@ module QA if quarantine_tag&.is_a?(Hash) && quarantine_tag&.key?(:only) # If the :quarantine hash contains :only, we respect that. # For instance `quarantine: { only: { subdomain: :staging } }` will only quarantine the test when it runs against staging. - return unless Runtime::Env.address_matches?(quarantine_tag[:only]) + return unless Runtime::Env.context_matches?(quarantine_tag[:only]) end skip(quarantine_message(quarantine_tag)) diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 08faacb6db3..5fc36b68e5c 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -40,7 +40,7 @@ module QA return_response_or_raise(e) end - def put(url, payload) + def put(url, payload = nil) RestClient::Request.execute( method: :put, url: url, diff --git a/qa/qa/support/json_formatter.rb b/qa/qa/support/json_formatter.rb index f6e40436ec8..0b805cd9eec 100644 --- a/qa/qa/support/json_formatter.rb +++ b/qa/qa/support/json_formatter.rb @@ -50,7 +50,8 @@ module QA pending_message: example.execution_result.pending_message, testcase: example.metadata[:testcase], quarantine: example.metadata[:quarantine], - screenshot: example.metadata[:screenshot] + screenshot: example.metadata[:screenshot], + ci_job_url: QA::Runtime::Env.ci_job_url } end diff --git a/qa/qa/support/otp.rb b/qa/qa/support/otp.rb index 0d7c394cf69..0a0dc64a726 100644 --- a/qa/qa/support/otp.rb +++ b/qa/qa/support/otp.rb @@ -13,11 +13,14 @@ module QA # Fetches a fresh OTP and returns it only after rotp provides the same OTP twice # An OTP is valid for 30 seconds so 70 attempts with 0.5 interval would ensure we complete 1 cycle - Support::Retrier.retry_until(max_attempts: 70, sleep_interval: 0.5) do + + QA::Runtime::Logger.debug("Fetching a fresh OTP...") + Support::Retrier.retry_until(max_attempts: 70, sleep_interval: 0.5, log: false) do otps << @rotp.now otps.size >= 3 && otps[-1] == otps[-2] && otps[-1] != otps[-3] end + QA::Runtime::Logger.debug("Fetched OTP: #{otps.last}") otps.last end end diff --git a/qa/qa/support/retrier.rb b/qa/qa/support/retrier.rb index f28534e7c11..25dbb42cf6f 100644 --- a/qa/qa/support/retrier.rb +++ b/qa/qa/support/retrier.rb @@ -34,15 +34,17 @@ module QA result end - def retry_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false) + def retry_until(max_attempts: nil, max_duration: nil, reload_page: nil, sleep_interval: 0, raise_on_failure: true, retry_on_exception: false, log: true) # For backwards-compatibility max_attempts = 3 if max_attempts.nil? && max_duration.nil? - start_msg ||= ["with retry_until:"] - start_msg << "max_attempts: #{max_attempts};" if max_attempts - start_msg << "max_duration: #{max_duration};" if max_duration - start_msg << "reload_page: #{reload_page}; sleep_interval: #{sleep_interval}; raise_on_failure: #{raise_on_failure}; retry_on_exception: #{retry_on_exception}" - QA::Runtime::Logger.debug(start_msg.join(' ')) + if log + start_msg ||= ["with retry_until:"] + start_msg << "max_attempts: #{max_attempts};" if max_attempts + start_msg << "max_duration: #{max_duration};" if max_duration + start_msg << "reload_page: #{reload_page}; sleep_interval: #{sleep_interval}; raise_on_failure: #{raise_on_failure}; retry_on_exception: #{retry_on_exception}" + QA::Runtime::Logger.debug(start_msg.join(' ')) + end result = nil repeat_until( @@ -51,7 +53,8 @@ module QA reload_page: reload_page, sleep_interval: sleep_interval, raise_on_failure: raise_on_failure, - retry_on_exception: retry_on_exception + retry_on_exception: retry_on_exception, + log: log ) do result = yield end diff --git a/qa/qa/support/run.rb b/qa/qa/support/run.rb new file mode 100644 index 00000000000..a91e7dfd2cb --- /dev/null +++ b/qa/qa/support/run.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require 'open3' + +module QA + module Support + module Run + include QA::Support::Repeater + + CommandError = Class.new(StandardError) + + Result = Struct.new(:command, :exitstatus, :response) do + alias_method :to_s, :response + + def success? + exitstatus == 0 && !response.include?('Error encountered') + end + end + + def run(command_str, env: [], max_attempts: 1, log_prefix: '') + command = [*env, command_str, '2>&1'].compact.join(' ') + result = nil + + repeat_until(max_attempts: max_attempts, raise_on_failure: false) do + Runtime::Logger.debug "#{log_prefix}pwd=[#{Dir.pwd}], command=[#{command}]" + output, status = Open3.capture2e(command) + output.chomp! + Runtime::Logger.debug "#{log_prefix}output=[#{output}], exitstatus=[#{status.exitstatus}]" + + result = Result.new(command, status.exitstatus, output) + + result.success? + end + + unless result.success? + raise CommandError, "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}" + end + + result + end + end + end +end diff --git a/qa/qa/support/ssh.rb b/qa/qa/support/ssh.rb new file mode 100644 index 00000000000..a5e8e96cb6c --- /dev/null +++ b/qa/qa/support/ssh.rb @@ -0,0 +1,62 @@ +# frozen_string_literal: true + +require 'tempfile' +require 'etc' + +module QA + module Support + class SSH + include Scenario::Actable + include Support::Run + + attr_accessor :known_hosts_file, :private_key_file, :key + attr_reader :uri + + def initialize + @private_key_file = Tempfile.new("id_#{SecureRandom.hex(8)}") + @known_hosts_file = Tempfile.new("known_hosts_#{SecureRandom.hex(8)}") + end + + def uri=(address) + @uri = URI(address) + end + + def setup(env: nil) + File.binwrite(private_key_file, key.private_key) + File.chmod(0700, private_key_file) + + keyscan_params = ['-H'] + keyscan_params << "-p #{uri_port}" if uri_port + keyscan_params << uri.host + + res = run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}", env: env, log_prefix: 'SSH: ') + return res.response unless res.success? + + true + end + + def delete + private_key_file.close(true) + known_hosts_file.close(true) + end + + def reset_2fa_codes + ssh_params = [uri.host] + ssh_params << "-p #{uri_port}" if uri_port + ssh_params << "2fa_recovery_codes" + + run("echo yes | ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path} #{git_user}@#{ssh_params.join(' ')}", log_prefix: 'SSH: ').to_s + end + + private + + def uri_port + uri.port && (uri.port != 80) ? uri.port : nil + end + + def git_user + QA::Runtime::Env.running_in_ci? || [443, 80].include?(uri.port) ? 'git' : Etc.getlogin + end + end + end +end diff --git a/qa/qa/support/wait_for_requests.rb b/qa/qa/support/wait_for_requests.rb index 943d7d510df..ebc473a7d86 100644 --- a/qa/qa/support/wait_for_requests.rb +++ b/qa/qa/support/wait_for_requests.rb @@ -9,18 +9,14 @@ module QA def wait_for_requests(skip_finished_loading_check: false) Waiter.wait_until(log: false) do - finished_all_ajax_requests? && finished_all_axios_requests? && (!skip_finished_loading_check ? finished_loading?(wait: 1) : true) + finished_all_ajax_requests? && (!skip_finished_loading_check ? finished_loading?(wait: 1) : true) end - end - - def finished_all_axios_requests? - Capybara.page.evaluate_script('window.pendingRequests || 0').zero? # rubocop:disable Style/NumericPredicate + rescue Repeater::WaitExceededError + raise $!, 'Page did not fully load. This could be due to an unending async request or loading icon.' end def finished_all_ajax_requests? - return true if Capybara.page.evaluate_script('typeof jQuery === "undefined"') - - Capybara.page.evaluate_script('jQuery.active').zero? # rubocop:disable Style/NumericPredicate + Capybara.page.evaluate_script('window.pendingRequests || window.pendingRailsUJSRequests || 0').zero? # rubocop:disable Style/NumericPredicate end def finished_loading?(wait: DEFAULT_MAX_WAIT_TIME) diff --git a/qa/qa/tools/initialize_gitlab_auth.rb b/qa/qa/tools/initialize_gitlab_auth.rb new file mode 100644 index 00000000000..b06ddcab040 --- /dev/null +++ b/qa/qa/tools/initialize_gitlab_auth.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +require_relative '../../qa' + +module QA + module Tools + # Task to set default password from Runtime::Env.default_password if not set already + # Also creates a personal access token + # @example + # $ bundle exec rake 'initialize_gitlab_auth[http://gitlab.test]' + class InitializeGitLabAuth + attr_reader :address + + def initialize(address:) + @address = address + end + + def run + Runtime::Scenario.define(:gitlab_address, address) + + puts "Signing in and creating the default password for the root user if it's not set already..." + QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login) + Flow::Login.sign_in + + puts "Creating an API scoped access token for the root user..." + puts "Token: #{Resource::PersonalAccessToken.fabricate!.access_token}" + end + end + end +end diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb index d59ee24c758..1adf3799b0e 100644 --- a/qa/spec/factory/resource/user_spec.rb +++ b/qa/spec/factory/resource/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::User do +RSpec.describe QA::Resource::User do describe "#fabricate_via_api!" do response = Struct.new(:code, :body) diff --git a/qa/spec/git/location_spec.rb b/qa/spec/git/location_spec.rb index 0c57291666f..ee714206e4f 100644 --- a/qa/spec/git/location_spec.rb +++ b/qa/spec/git/location_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Git::Location do +RSpec.describe QA::Git::Location do describe '.new' do context 'when URI starts with ssh://' do context 'when URI has port' do diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index 8355c77f493..02bb7783c28 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -1,12 +1,21 @@ # frozen_string_literal: true -describe QA::Git::Repository do +RSpec.describe QA::Git::Repository do include Helpers::StubENV shared_context 'unresolvable git directory' do let(:repo_uri) { 'http://foo/bar.git' } let(:repo_uri_with_credentials) { 'http://root@foo/bar.git' } - let(:repository) { described_class.new.tap { |r| r.uri = repo_uri } } + let(:env_vars) { [%q{HOME="temp"}] } + let(:extra_env_vars) { [] } + let(:run_params) { { env: env_vars + extra_env_vars, log_prefix: "Git: " } } + let(:repository) do + described_class.new.tap do |r| + r.uri = repo_uri + r.env_vars = env_vars + end + end + let(:tmp_git_dir) { Dir.mktmpdir } let(:tmp_netrc_dir) { Dir.mktmpdir } @@ -28,14 +37,13 @@ describe QA::Git::Repository do end shared_examples 'command with retries' do - let(:extra_args) { {} } let(:result_output) { +'Command successful' } let(:result) { described_class::Result.new(any_args, 0, result_output) } let(:command_return) { result_output } context 'when command is successful' do it 'returns the #run command Result output' do - expect(repository).to receive(:run).with(command, extra_args.merge(max_attempts: 3)).and_return(result) + expect(repository).to receive(:run).with(command, run_params.merge(max_attempts: 3)).and_return(result) expect(call_method).to eq(command_return) end @@ -52,10 +60,10 @@ describe QA::Git::Repository do end context 'and retried command is not successful after 3 attempts' do - it 'raises a RepositoryCommandError exception' do + it 'raises a CommandError exception' do expect(Open3).to receive(:capture2e).and_return([+'FAILURE', double(exitstatus: 42)]).exactly(3).times - expect { call_method }.to raise_error(described_class::RepositoryCommandError, /The command .* failed \(42\) with the following output:\nFAILURE/) + expect { call_method }.to raise_error(QA::Support::Run::CommandError, /The command .* failed \(42\) with the following output:\nFAILURE/) end end end @@ -117,12 +125,72 @@ describe QA::Git::Repository do let(:call_method) { repository.push_changes(branch) } end end + + context 'with push options' do + let(:command) { "git push #{push_options} #{repo_uri_with_credentials} #{branch}" } + + context 'when set to create a merge request' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.create' } + let(:call_method) { repository.push_changes(push_options: { create: true }) } + end + end + + context 'when set to merge when pipeline succeeds' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.merge_when_pipeline_succeeds' } + let(:call_method) { repository.push_changes(push_options: { merge_when_pipeline_succeeds: true }) } + end + end + + context 'when set to remove source branch' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.remove_source_branch' } + let(:call_method) { repository.push_changes(push_options: { remove_source_branch: true }) } + end + end + + context 'when title is given' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.title="Is A Title"' } + let(:call_method) { repository.push_changes(push_options: { title: 'Is A Title' }) } + end + end + + context 'when description is given' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.description="Is A Description"' } + let(:call_method) { repository.push_changes(push_options: { description: 'Is A Description' }) } + end + end + + context 'when target branch is given' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.target="is-a-target-branch"' } + let(:call_method) { repository.push_changes(push_options: { target: 'is-a-target-branch' }) } + end + end + + context 'when a label is given' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.label="is-a-label"' } + let(:call_method) { repository.push_changes(push_options: { label: ['is-a-label'] }) } + end + end + + context 'when two labels are given' do + it_behaves_like 'command with retries' do + let(:push_options) { '-o merge_request.label="is-a-label" -o merge_request.label="is-another-label"' } + let(:call_method) { repository.push_changes(push_options: { label: %w[is-a-label is-another-label] }) } + end + end + end end describe '#git_protocol=' do [0, 1, 2].each do |version| it "configures git to use protocol version #{version}" do - expect(repository).to receive(:run).with("git config protocol.version #{version}") + expect(repository).to receive(:run).with("git config protocol.version #{version}", run_params.merge(max_attempts: 1)) repository.git_protocol = version end @@ -140,7 +208,7 @@ describe QA::Git::Repository do let(:command) { "git ls-remote #{repo_uri_with_credentials}" } let(:result_output) { +'packet: git< version 2' } let(:command_return) { '2' } - let(:extra_args) { { env: "GIT_TRACE_PACKET=1" } } + let(:extra_env_vars) { ["GIT_TRACE_PACKET=1"] } end it "reports the detected version" do diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 0cbb0a2b12e..52345876149 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Page::Base do +RSpec.describe QA::Page::Base do describe 'page helpers' do it 'exposes helpful page helpers' do expect(subject).to respond_to :refresh, :wait_until, :scroll_to diff --git a/qa/spec/page/element_spec.rb b/qa/spec/page/element_spec.rb index 3f64743ffac..fbf58b5e18a 100644 --- a/qa/spec/page/element_spec.rb +++ b/qa/spec/page/element_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Page::Element do +RSpec.describe QA::Page::Element do describe '#selector' do it 'transforms element name into QA-specific selector' do expect(described_class.new(:sign_in_button).selector) diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb index f2ce9fb2cf8..df3447770be 100644 --- a/qa/spec/page/logging_spec.rb +++ b/qa/spec/page/logging_spec.rb @@ -3,7 +3,7 @@ require 'capybara/dsl' require 'logger' -describe QA::Support::Page::Logging do +RSpec.describe QA::Support::Page::Logging do let(:page) { double.as_null_object } before do diff --git a/qa/spec/page/validator_spec.rb b/qa/spec/page/validator_spec.rb index c727cfb686e..cfb36052294 100644 --- a/qa/spec/page/validator_spec.rb +++ b/qa/spec/page/validator_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Page::Validator do +RSpec.describe QA::Page::Validator do describe '#constants' do subject do described_class.new(QA::Page::Project) diff --git a/qa/spec/page/view_spec.rb b/qa/spec/page/view_spec.rb index 3cb64dcd9c2..3342b387ed1 100644 --- a/qa/spec/page/view_spec.rb +++ b/qa/spec/page/view_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Page::View do +RSpec.describe QA::Page::View do let(:element) do double('element', name: :something, pattern: /some element/) end diff --git a/qa/spec/resource/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb index eb2bdd1be64..69a95c92332 100644 --- a/qa/spec/resource/api_fabricator_spec.rb +++ b/qa/spec/resource/api_fabricator_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::ApiFabricator do +RSpec.describe QA::Resource::ApiFabricator do let(:resource_without_api_support) do Class.new do def self.name diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index b23de19e1f8..c0bedf794be 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::Base do +RSpec.describe QA::Resource::Base do include Helpers::StubENV let(:resource) { spy('resource') } diff --git a/qa/spec/resource/events/base_spec.rb b/qa/spec/resource/events/base_spec.rb index 9cdf4785092..4df30a970fc 100644 --- a/qa/spec/resource/events/base_spec.rb +++ b/qa/spec/resource/events/base_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::Events::Base do +RSpec.describe QA::Resource::Events::Base do let(:resource) do Class.new(QA::Resource::Base) do def api_get_path diff --git a/qa/spec/resource/events/project_spec.rb b/qa/spec/resource/events/project_spec.rb index 98da87906fa..88d50749a0a 100644 --- a/qa/spec/resource/events/project_spec.rb +++ b/qa/spec/resource/events/project_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::Events::Project do +RSpec.describe QA::Resource::Events::Project do let(:resource) do Class.new(QA::Resource::Base) do def api_get_path diff --git a/qa/spec/resource/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb index 2f9e4958ae1..2b9c90b3dac 100644 --- a/qa/spec/resource/repository/push_spec.rb +++ b/qa/spec/resource/repository/push_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::Repository::Push do +RSpec.describe QA::Resource::Repository::Push do describe '.files=' do let(:files) do [ diff --git a/qa/spec/resource/ssh_key_spec.rb b/qa/spec/resource/ssh_key_spec.rb index b2b5ec070e1..fd0fda3c1b8 100644 --- a/qa/spec/resource/ssh_key_spec.rb +++ b/qa/spec/resource/ssh_key_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::SSHKey do +RSpec.describe QA::Resource::SSHKey do describe '#key' do it 'generates a default key' do expect(subject.key).to be_a(QA::Runtime::Key::RSA) diff --git a/qa/spec/resource/user_spec.rb b/qa/spec/resource/user_spec.rb index 5845f7996a3..e7397d9c0bf 100644 --- a/qa/spec/resource/user_spec.rb +++ b/qa/spec/resource/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Resource::User do +RSpec.describe QA::Resource::User do let(:api_resource) do { name: "GitLab QA", diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb index 6f7020d6595..dd139fda980 100644 --- a/qa/spec/runtime/api/client_spec.rb +++ b/qa/spec/runtime/api/client_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::API::Client do +RSpec.describe QA::Runtime::API::Client do include Helpers::StubENV describe 'initialization' do diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb index 8354eff6234..93de2f4a87e 100644 --- a/qa/spec/runtime/api/request_spec.rb +++ b/qa/spec/runtime/api/request_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::API::Request do +RSpec.describe QA::Runtime::API::Request do let(:client) { QA::Runtime::API::Client.new('http://example.com') } let(:request) { described_class.new(client, '/users') } diff --git a/qa/spec/runtime/application_settings_spec.rb b/qa/spec/runtime/application_settings_spec.rb index e48214b22e6..5c4947f6f70 100644 --- a/qa/spec/runtime/application_settings_spec.rb +++ b/qa/spec/runtime/application_settings_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::ApplicationSettings do +RSpec.describe QA::Runtime::ApplicationSettings do let(:api_client) { double('QA::Runtime::API::Client') } let(:request) { Struct.new(:url).new('http://api') } let(:get_response) { Struct.new(:body).new("{}") } diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index 0cfb9a70c88..3396ae6f0b8 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Env do +RSpec.describe QA::Runtime::Env do include Helpers::StubENV shared_examples 'boolean method' do |**kwargs| @@ -341,7 +341,7 @@ describe QA::Runtime::Env do end end - describe '.address_matches?' do + describe '.context_matches?' do it 'returns true when url has .com' do QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") @@ -364,24 +364,24 @@ describe QA::Runtime::Env do it 'matches multiple subdomains' do QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") - expect(described_class.address_matches?(subdomain: [:release, :staging])).to be_truthy - expect(described_class.address_matches?(:production, subdomain: [:release, :staging])).to be_truthy + expect(described_class.context_matches?(subdomain: [:release, :staging])).to be_truthy + expect(described_class.context_matches?(:production, subdomain: [:release, :staging])).to be_truthy end it 'matches :production' do QA::Runtime::Scenario.define(:gitlab_address, "https://gitlab.com/") - expect(described_class.address_matches?(:production)).to be_truthy + expect(described_class.context_matches?(:production)).to be_truthy end it 'doesnt match with mismatching switches' do QA::Runtime::Scenario.define(:gitlab_address, 'https://gitlab.test') aggregate_failures do - expect(described_class.address_matches?(tld: '.net')).to be_falsey - expect(described_class.address_matches?(:production)).to be_falsey - expect(described_class.address_matches?(subdomain: [:staging])).to be_falsey - expect(described_class.address_matches?(domain: 'example')).to be_falsey + expect(described_class.context_matches?(tld: '.net')).to be_falsey + expect(described_class.context_matches?(:production)).to be_falsey + expect(described_class.context_matches?(subdomain: [:staging])).to be_falsey + expect(described_class.context_matches?(domain: 'example')).to be_falsey end end end @@ -389,7 +389,7 @@ describe QA::Runtime::Env do it 'returns false for mismatching' do QA::Runtime::Scenario.define(:gitlab_address, "https://staging.gitlab.com") - expect(described_class.address_matches?(:production)).to be_falsey + expect(described_class.context_matches?(:production)).to be_falsey end end end diff --git a/qa/spec/runtime/feature_spec.rb b/qa/spec/runtime/feature_spec.rb index db3c2f65963..39c20dd3070 100644 --- a/qa/spec/runtime/feature_spec.rb +++ b/qa/spec/runtime/feature_spec.rb @@ -1,87 +1,215 @@ # frozen_string_literal: true -describe QA::Runtime::Feature do +RSpec.describe QA::Runtime::Feature do let(:api_client) { double('QA::Runtime::API::Client') } let(:request) { Struct.new(:url).new('http://api') } let(:response_post) { Struct.new(:code).new(201) } - let(:response_get) { Struct.new(:code, :body).new(200, '[{ "name": "a-flag", "state": "on" }]') } before do allow(described_class).to receive(:api_client).and_return(api_client) end - describe '.enable' do - it 'enables a feature flag' do - expect(QA::Runtime::API::Request) - .to receive(:new) - .with(api_client, "/features/a-flag") - .and_return(request) - expect(described_class) - .to receive(:post) - .with(request.url, { value: true }) - .and_return(response_post) - - subject.enable('a-flag') - end + where(:feature_flag) do + ['a_flag', :a_flag] end - describe '.enable_and_verify' do - it 'enables a feature flag' do - allow(described_class).to receive(:get).and_return(response_get) + with_them do + shared_examples 'enables a feature flag' do + it 'enables a feature flag for a scope' do + allow(described_class).to receive(:get) + .and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "on" }]')) - expect(QA::Runtime::API::Request).to receive(:new) - .with(api_client, "/features/a-flag").and_return(request) - expect(described_class).to receive(:post) - .with(request.url, { value: true }).and_return(response_post) - expect(QA::Runtime::API::Request).to receive(:new) - .with(api_client, "/features").and_return(request) + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features/a_flag").and_return(request) + expect(described_class).to receive(:post) + .with(request.url, { value: true, scope => actor_name }).and_return(response_post) + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features").and_return(request) + expect(QA::Runtime::Logger).to receive(:info).with("Enabling feature: a_flag for scope \"#{scope}: #{actor_name}\"") + expect(QA::Runtime::Logger).to receive(:info).with("Successfully enabled and verified feature flag: a_flag") - subject.enable_and_verify('a-flag') + described_class.enable(feature_flag, scope => actor) + end end - end - describe '.disable' do - it 'disables a feature flag' do - expect(QA::Runtime::API::Request) - .to receive(:new) - .with(api_client, "/features/a-flag") - .and_return(request) - expect(described_class) - .to receive(:post) - .with(request.url, { value: false }) - .and_return(response_post) - - subject.disable('a-flag') + shared_examples 'disables a feature flag' do + it 'disables a feature flag for a scope' do + allow(described_class).to receive(:get) + .and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "off" }]')) + + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features/a_flag").and_return(request) + expect(described_class).to receive(:post) + .with(request.url, { value: false, scope => actor_name }).and_return(response_post) + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features").and_return(request) + expect(QA::Runtime::Logger).to receive(:info).with("Disabling feature: a_flag for scope \"#{scope}: #{actor_name}\"") + expect(QA::Runtime::Logger).to receive(:info).with("Successfully disabled and verified feature flag: a_flag") + + described_class.disable(feature_flag, scope => actor ) + end end - end - describe '.disable_and_verify' do - it 'disables a feature flag' do - allow(described_class).to receive(:get) - .and_return(Struct.new(:code, :body).new(200, '[{ "name": "a-flag", "state": "off" }]')) + shared_examples 'checks a feature flag' do + context 'when the flag is enabled for a scope' do + it 'returns the feature flag state' do + expect(QA::Runtime::API::Request) + .to receive(:new) + .with(api_client, "/features") + .and_return(request) + expect(described_class) + .to receive(:get) + .and_return(Struct.new(:code, :body).new(200, %Q([{ "name": "a_flag", "state": "conditional", "gates": #{gates} }]))) + + expect(described_class.enabled?(feature_flag, scope => actor)).to be_truthy + end + end + end - expect(QA::Runtime::API::Request).to receive(:new) - .with(api_client, "/features/a-flag").and_return(request) - expect(described_class).to receive(:post) - .with(request.url, { value: false }).and_return(response_post) - expect(QA::Runtime::API::Request).to receive(:new) - .with(api_client, "/features").and_return(request) + describe '.enable' do + it 'enables a feature flag' do + allow(described_class).to receive(:get) + .and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "on" }]')) - subject.disable_and_verify('a-flag') + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features/a_flag").and_return(request) + expect(described_class).to receive(:post) + .with(request.url, { value: true }).and_return(response_post) + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features").and_return(request) + + described_class.enable(feature_flag) + end + + context 'when a project scope is provided' do + it_behaves_like 'enables a feature flag' do + let(:scope) { :project } + let(:actor_name) { 'group-name/project-name' } + let(:actor) { Struct.new(:full_path).new(actor_name) } + end + end + + context 'when a group scope is provided' do + it_behaves_like 'enables a feature flag' do + let(:scope) { :group } + let(:actor_name) { 'group-name' } + let(:actor) { Struct.new(:full_path).new(actor_name) } + end + end + + context 'when a user scope is provided' do + it_behaves_like 'enables a feature flag' do + let(:scope) { :user } + let(:actor_name) { 'user-name' } + let(:actor) { Struct.new(:username).new(actor_name) } + end + end + + context 'when a feature group scope is provided' do + it_behaves_like 'enables a feature flag' do + let(:scope) { :feature_group } + let(:actor_name) { 'foo' } + let(:actor) { "foo" } + end + end end - end - describe '.enabled?' do - it 'returns a feature flag state' do - expect(QA::Runtime::API::Request) - .to receive(:new) - .with(api_client, "/features") - .and_return(request) - expect(described_class) - .to receive(:get) - .and_return(response_get) - - expect(subject.enabled?('a-flag')).to be_truthy + describe '.disable' do + it 'disables a feature flag' do + allow(described_class).to receive(:get) + .and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "off" }]')) + + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features/a_flag").and_return(request) + expect(described_class).to receive(:post) + .with(request.url, { value: false }).and_return(response_post) + expect(QA::Runtime::API::Request).to receive(:new) + .with(api_client, "/features").and_return(request) + + described_class.disable(feature_flag) + end + + context 'when a project scope is provided' do + it_behaves_like 'disables a feature flag' do + let(:scope) { :project } + let(:actor_name) { 'group-name/project-name' } + let(:actor) { Struct.new(:full_path).new(actor_name) } + end + end + + context 'when a group scope is provided' do + it_behaves_like 'disables a feature flag' do + let(:scope) { :group } + let(:actor_name) { 'group-name' } + let(:actor) { Struct.new(:full_path).new(actor_name) } + end + end + + context 'when a user scope is provided' do + it_behaves_like 'disables a feature flag' do + let(:scope) { :user } + let(:actor_name) { 'user-name' } + let(:actor) { Struct.new(:username).new(actor_name) } + end + end + + context 'when a feature group scope is provided' do + it_behaves_like 'disables a feature flag' do + let(:scope) { :feature_group } + let(:actor_name) { 'foo' } + let(:actor) { "foo" } + end + end + end + + describe '.enabled?' do + it 'returns a feature flag state' do + expect(QA::Runtime::API::Request) + .to receive(:new) + .with(api_client, "/features") + .and_return(request) + expect(described_class) + .to receive(:get) + .and_return(Struct.new(:code, :body).new(200, '[{ "name": "a_flag", "state": "on" }]')) + + expect(described_class.enabled?(feature_flag)).to be_truthy + end + + context 'when a project scope is provided' do + it_behaves_like 'checks a feature flag' do + let(:scope) { :project } + let(:actor_name) { 'group-name/project-name' } + let(:actor) { Struct.new(:full_path, :id).new(actor_name, 270) } + let(:gates) { %q([{"key": "actors", "value": ["Project:270"]}]) } + end + end + + context 'when a group scope is provided' do + it_behaves_like 'checks a feature flag' do + let(:scope) { :group } + let(:actor_name) { 'group-name' } + let(:actor) { Struct.new(:full_path, :id).new(actor_name, 33) } + let(:gates) { %q([{"key": "actors", "value": ["Group:33"]}]) } + end + end + + context 'when a user scope is provided' do + it_behaves_like 'checks a feature flag' do + let(:scope) { :user } + let(:actor_name) { 'user-name' } + let(:actor) { Struct.new(:full_path, :id).new(actor_name, 13) } + let(:gates) { %q([{"key": "actors", "value": ["User:13"]}]) } + end + end + + context 'when a feature group scope is provided' do + it_behaves_like 'checks a feature flag' do + let(:scope) { :feature_group } + let(:actor_name) { 'foo' } + let(:actor) { "foo" } + let(:gates) { %q([{"key": "groups", "value": ["foo"]}]) } + end + end end end end diff --git a/qa/spec/runtime/key/ecdsa_spec.rb b/qa/spec/runtime/key/ecdsa_spec.rb index 3f9718e62c5..499233df618 100644 --- a/qa/spec/runtime/key/ecdsa_spec.rb +++ b/qa/spec/runtime/key/ecdsa_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Key::ECDSA do +RSpec.describe QA::Runtime::Key::ECDSA do describe '#public_key' do [256, 384, 521].each do |bits| it "generates a public #{bits}-bits ECDSA key" do diff --git a/qa/spec/runtime/key/ed25519_spec.rb b/qa/spec/runtime/key/ed25519_spec.rb index 08f232260af..e63c7f5deae 100644 --- a/qa/spec/runtime/key/ed25519_spec.rb +++ b/qa/spec/runtime/key/ed25519_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Key::ED25519 do +RSpec.describe QA::Runtime::Key::ED25519 do describe '#public_key' do subject { described_class.new.public_key } diff --git a/qa/spec/runtime/key/rsa_spec.rb b/qa/spec/runtime/key/rsa_spec.rb index fcb52f541bf..5b5d8a13fa1 100644 --- a/qa/spec/runtime/key/rsa_spec.rb +++ b/qa/spec/runtime/key/rsa_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Key::RSA do +RSpec.describe QA::Runtime::Key::RSA do describe '#public_key' do subject { described_class.new.public_key } diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb index 44be3381bff..a888bf1452b 100644 --- a/qa/spec/runtime/logger_spec.rb +++ b/qa/spec/runtime/logger_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Logger do +RSpec.describe QA::Runtime::Logger do before do logger = Logger.new $stdout logger.level = ::Logger::DEBUG diff --git a/qa/spec/runtime/namespace_spec.rb b/qa/spec/runtime/namespace_spec.rb index d24fa509f30..92836862864 100644 --- a/qa/spec/runtime/namespace_spec.rb +++ b/qa/spec/runtime/namespace_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Namespace do +RSpec.describe QA::Runtime::Namespace do include Helpers::StubENV describe '.name' do diff --git a/qa/spec/runtime/release_spec.rb b/qa/spec/runtime/release_spec.rb index b5a7dd5269d..b4e278fb546 100644 --- a/qa/spec/runtime/release_spec.rb +++ b/qa/spec/runtime/release_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Release do +RSpec.describe QA::Runtime::Release do context 'when release version has extension strategy' do let(:strategy) { spy('strategy') } diff --git a/qa/spec/runtime/scenario_spec.rb b/qa/spec/runtime/scenario_spec.rb index 30ada4529ed..175973b6795 100644 --- a/qa/spec/runtime/scenario_spec.rb +++ b/qa/spec/runtime/scenario_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Runtime::Scenario do +RSpec.describe QA::Runtime::Scenario do subject do Module.new.extend(described_class) end diff --git a/qa/spec/scenario/actable_spec.rb b/qa/spec/scenario/actable_spec.rb index 589d0c61993..36e9f3de961 100644 --- a/qa/spec/scenario/actable_spec.rb +++ b/qa/spec/scenario/actable_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Actable do +RSpec.describe QA::Scenario::Actable do subject do Class.new do include QA::Scenario::Actable diff --git a/qa/spec/scenario/bootable_spec.rb b/qa/spec/scenario/bootable_spec.rb index e8accb45518..8a96e9bebbf 100644 --- a/qa/spec/scenario/bootable_spec.rb +++ b/qa/spec/scenario/bootable_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Bootable do +RSpec.describe QA::Scenario::Bootable do subject do Class.new(QA::Scenario::Template) .include(described_class) diff --git a/qa/spec/scenario/template_spec.rb b/qa/spec/scenario/template_spec.rb index 65793734548..f07d817ea16 100644 --- a/qa/spec/scenario/template_spec.rb +++ b/qa/spec/scenario/template_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Template do +RSpec.describe QA::Scenario::Template do let(:feature) { spy('Runtime::Feature') } let(:release) { spy('Runtime::Release') } diff --git a/qa/spec/scenario/test/instance/airgapped_spec.rb b/qa/spec/scenario/test/instance/airgapped_spec.rb index 0c4167eafff..5e319ba4bbb 100644 --- a/qa/spec/scenario/test/instance/airgapped_spec.rb +++ b/qa/spec/scenario/test/instance/airgapped_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Instance::Airgapped do +RSpec.describe QA::Scenario::Test::Instance::Airgapped do describe '#perform' do it_behaves_like 'a QA scenario class' do end diff --git a/qa/spec/scenario/test/instance/all_spec.rb b/qa/spec/scenario/test/instance/all_spec.rb index 8acd56914c5..875df9a32f5 100644 --- a/qa/spec/scenario/test/instance/all_spec.rb +++ b/qa/spec/scenario/test/instance/all_spec.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Instance::All do +RSpec.describe QA::Scenario::Test::Instance::All do it_behaves_like 'a QA scenario class' end diff --git a/qa/spec/scenario/test/instance/smoke_spec.rb b/qa/spec/scenario/test/instance/smoke_spec.rb index 6cc71699be9..09d0df2c479 100644 --- a/qa/spec/scenario/test/instance/smoke_spec.rb +++ b/qa/spec/scenario/test/instance/smoke_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Instance::Smoke do +RSpec.describe QA::Scenario::Test::Instance::Smoke do it_behaves_like 'a QA scenario class' do let(:tags) { [:smoke] } end diff --git a/qa/spec/scenario/test/integration/github_spec.rb b/qa/spec/scenario/test/integration/github_spec.rb index b2d577bd552..b68b06a7b9f 100644 --- a/qa/spec/scenario/test/integration/github_spec.rb +++ b/qa/spec/scenario/test/integration/github_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::Github do +RSpec.describe QA::Scenario::Test::Integration::Github do describe '#perform' do let(:env) { spy('Runtime::Env') } diff --git a/qa/spec/scenario/test/integration/instance_saml_spec.rb b/qa/spec/scenario/test/integration/instance_saml_spec.rb index 15f15b2e643..20e860d3e4b 100644 --- a/qa/spec/scenario/test/integration/instance_saml_spec.rb +++ b/qa/spec/scenario/test/integration/instance_saml_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::InstanceSAML do +RSpec.describe QA::Scenario::Test::Integration::InstanceSAML do describe '#perform' do it_behaves_like 'a QA scenario class' do let(:tags) { [:instance_saml] } diff --git a/qa/spec/scenario/test/integration/kubernetes_spec.rb b/qa/spec/scenario/test/integration/kubernetes_spec.rb index 51ee7b9acff..d5885b97343 100644 --- a/qa/spec/scenario/test/integration/kubernetes_spec.rb +++ b/qa/spec/scenario/test/integration/kubernetes_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::Kubernetes do +RSpec.describe QA::Scenario::Test::Integration::Kubernetes do describe '#perform' do it_behaves_like 'a QA scenario class' do let(:tags) { [:kubernetes] } diff --git a/qa/spec/scenario/test/integration/ldap_spec.rb b/qa/spec/scenario/test/integration/ldap_spec.rb index c493cde6c7a..c53302d9bd3 100644 --- a/qa/spec/scenario/test/integration/ldap_spec.rb +++ b/qa/spec/scenario/test/integration/ldap_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::LDAPNoTLS do +RSpec.describe QA::Scenario::Test::Integration::LDAPNoTLS do describe '#perform' do it_behaves_like 'a QA scenario class' do let(:tags) { [:ldap_no_tls] } @@ -8,7 +8,7 @@ describe QA::Scenario::Test::Integration::LDAPNoTLS do end end -describe QA::Scenario::Test::Integration::LDAPNoServer do +RSpec.describe QA::Scenario::Test::Integration::LDAPNoServer do describe '#perform' do it_behaves_like 'a QA scenario class' do let(:tags) { [:ldap_no_server] } @@ -16,7 +16,7 @@ describe QA::Scenario::Test::Integration::LDAPNoServer do end end -describe QA::Scenario::Test::Integration::LDAPTLS do +RSpec.describe QA::Scenario::Test::Integration::LDAPTLS do describe '#perform' do it_behaves_like 'a QA scenario class' do let(:tags) { [:ldap_tls] } diff --git a/qa/spec/scenario/test/integration/mattermost_spec.rb b/qa/spec/scenario/test/integration/mattermost_spec.rb index 7e4eb6284e8..9532ec35b95 100644 --- a/qa/spec/scenario/test/integration/mattermost_spec.rb +++ b/qa/spec/scenario/test/integration/mattermost_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::Mattermost do +RSpec.describe QA::Scenario::Test::Integration::Mattermost do describe '#perform' do it_behaves_like 'a QA scenario class' do let(:args) { %w[gitlab_address mattermost_address] } diff --git a/qa/spec/scenario/test/integration/object_storage_spec.rb b/qa/spec/scenario/test/integration/object_storage_spec.rb index 8b4367bee32..235dd495687 100644 --- a/qa/spec/scenario/test/integration/object_storage_spec.rb +++ b/qa/spec/scenario/test/integration/object_storage_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::ObjectStorage do +RSpec.describe QA::Scenario::Test::Integration::ObjectStorage do describe '#perform' do it_behaves_like 'a QA scenario class' do let(:tags) { [:object_storage] } diff --git a/qa/spec/scenario/test/sanity/framework_spec.rb b/qa/spec/scenario/test/sanity/framework_spec.rb index a63c59e2995..5ae8b123ec2 100644 --- a/qa/spec/scenario/test/sanity/framework_spec.rb +++ b/qa/spec/scenario/test/sanity/framework_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Sanity::Framework do +RSpec.describe QA::Scenario::Test::Sanity::Framework do it_behaves_like 'a QA scenario class' do let(:tags) { [:framework] } end diff --git a/qa/spec/scenario/test/sanity/selectors_spec.rb b/qa/spec/scenario/test/sanity/selectors_spec.rb index e18babaed63..2a68dd23219 100644 --- a/qa/spec/scenario/test/sanity/selectors_spec.rb +++ b/qa/spec/scenario/test/sanity/selectors_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Sanity::Selectors do +RSpec.describe QA::Scenario::Test::Sanity::Selectors do let(:validator) { spy('validator') } before do diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index 9686a9771c4..41bc3eadff4 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -36,7 +36,9 @@ RSpec.configure do |c| end end -describe QA::Specs::Helpers::Quarantine do +RSpec.describe QA::Specs::Helpers::Quarantine do + include Helpers::StubENV + describe '.skip_or_run_quarantined_contexts' do context 'with no tag focused' do before do @@ -312,7 +314,7 @@ describe QA::Specs::Helpers::Quarantine do end end - describe 'running against specific environments' do + describe 'running against specific environments or pipelines' do before do QA::Runtime::Scenario.define(:gitlab_address, 'https://staging.gitlab.com') described_class.configure_rspec @@ -400,5 +402,70 @@ describe QA::Specs::Helpers::Quarantine do expect(group.examples.first.execution_result.pending_message).to match(/[Tt]est.*not compatible.*environment/) end + + context 'with pipeline constraints' do + context 'without CI_PROJECT_NAME set' do + before do + stub_env('CI_PROJECT_NAME', nil) + described_class.configure_rspec + end + + it 'runs on any pipeline' do + group = describe_successfully do + it('runs given a single named pipeline', only: { pipeline: :nightly } ) {} + it('runs given an array of pipelines', only: { pipeline: [:canary, :not_nightly] }) {} + end + + aggregate_failures do + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:passed) + end + end + end + + context 'when a pipeline triggered from master runs in gitlab-qa' do + before do + stub_env('CI_PROJECT_NAME', 'gitlab-qa') + described_class.configure_rspec + end + + it 'runs on master pipelines' do + group = describe_successfully do + it('runs on master pipeline given a single pipeline', only: { pipeline: :master } ) {} + it('runs in master given an array of pipelines', only: { pipeline: [:canary, :master] }) {} + it('does not run in non-master pipelines', only: { pipeline: [:nightly, :not_nightly, :not_master] } ) {} + end + + aggregate_failures do + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:passed) + expect(group.examples[2].execution_result.status).to eq(:pending) + end + end + end + + context 'with CI_PROJECT_NAME set' do + before do + stub_env('CI_PROJECT_NAME', 'NIGHTLY') + described_class.configure_rspec + end + + it 'runs on designated pipeline' do + group = describe_successfully do + it('runs on nightly', only: { pipeline: :nightly } ) {} + it('does not run in not_nightly', only: { pipeline: :not_nightly } ) {} + it('runs on nightly given an array', only: { pipeline: [:canary, :nightly] }) {} + it('does not run in not_nightly given an array', only: { pipeline: [:not_nightly, :canary] }) {} + end + + aggregate_failures do + expect(group.examples[0].execution_result.status).to eq(:passed) + expect(group.examples[1].execution_result.status).to eq(:pending) + expect(group.examples[2].execution_result.status).to eq(:passed) + expect(group.examples[3].execution_result.status).to eq(:pending) + end + end + end + end end end diff --git a/qa/spec/specs/parallel_runner_spec.rb b/qa/spec/specs/parallel_runner_spec.rb index 67d94a1f648..c2d28bf81fb 100644 --- a/qa/spec/specs/parallel_runner_spec.rb +++ b/qa/spec/specs/parallel_runner_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Specs::ParallelRunner do +RSpec.describe QA::Specs::ParallelRunner do include Helpers::StubENV before do diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index 361588fa14f..8171cfcb3e6 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -2,7 +2,7 @@ require 'active_support/core_ext/hash' -describe QA::Specs::Runner do +RSpec.describe QA::Specs::Runner do shared_examples 'excludes orchestrated' do it 'excludes the orchestrated tag and includes default args' do expect_rspec_runner_arguments(['--tag', '~orchestrated', *described_class::DEFAULT_TEST_PATH_ARGS]) diff --git a/qa/spec/support/repeater_spec.rb b/qa/spec/support/repeater_spec.rb index b5d5058ef49..18ccbf250cb 100644 --- a/qa/spec/support/repeater_spec.rb +++ b/qa/spec/support/repeater_spec.rb @@ -4,7 +4,7 @@ require 'logger' require 'timecop' require 'active_support/core_ext/integer/time' -describe QA::Support::Repeater do +RSpec.describe QA::Support::Repeater do before do logger = ::Logger.new $stdout logger.level = ::Logger::DEBUG diff --git a/qa/spec/support/retrier_spec.rb b/qa/spec/support/retrier_spec.rb index ef1d53e6b65..6f052519516 100644 --- a/qa/spec/support/retrier_spec.rb +++ b/qa/spec/support/retrier_spec.rb @@ -3,7 +3,7 @@ require 'logger' require 'timecop' -describe QA::Support::Retrier do +RSpec.describe QA::Support::Retrier do before do logger = ::Logger.new $stdout logger.level = ::Logger::DEBUG diff --git a/qa/spec/support/run_spec.rb b/qa/spec/support/run_spec.rb new file mode 100644 index 00000000000..62eed71012e --- /dev/null +++ b/qa/spec/support/run_spec.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +RSpec.describe QA::Support::Run do + let(:class_instance) { (Class.new { include QA::Support::Run }).new } + let(:response) { 'successful response' } + let(:command) { 'some command' } + let(:expected_result) { described_class::Result.new("#{command} 2>&1", 0, response) } + + it 'runs successfully' do + expect(Open3).to receive(:capture2e).and_return([+response, double(exitstatus: 0)]) + + expect(class_instance.run(command)).to eq(expected_result) + end + + it 'retries twice and succeeds the third time' do + allow(Open3).to receive(:capture2e).and_return([+'', double(exitstatus: 1)]).twice + allow(Open3).to receive(:capture2e).and_return([+response, double(exitstatus: 0)]) + + expect(class_instance.run(command)).to eq(expected_result) + end + + it 'raises an exception on 3rd failure' do + allow(Open3).to receive(:capture2e).and_return([+'FAILURE', double(exitstatus: 1)]).thrice + + expect { class_instance.run(command) }.to raise_error(QA::Support::Run::CommandError, /The command .* failed \(1\) with the following output:\nFAILURE/) + end +end diff --git a/qa/spec/support/ssh_spec.rb b/qa/spec/support/ssh_spec.rb new file mode 100644 index 00000000000..f4d382f8adc --- /dev/null +++ b/qa/spec/support/ssh_spec.rb @@ -0,0 +1,114 @@ +# frozen_string_literal: true + +RSpec.describe QA::Support::SSH do + let(:key) { Struct.new(:private_key).new('private_key') } + let(:known_hosts_file) { Tempfile.new('known_hosts_file') } + let(:private_key_file) { Tempfile.new('private_key_file') } + let(:result) { QA::Support::Run::Result.new('', 0, '') } + + let(:ssh) do + described_class.new.tap do |ssh| + ssh.uri = uri + ssh.key = key + ssh.private_key_file = private_key_file + ssh.known_hosts_file = known_hosts_file + end + end + + shared_examples 'providing correct ports' do + context 'when no port specified in uri' do + let(:uri) { 'http://foo.com' } + + it 'does not provide port in ssh command' do + expect(ssh).to receive(:run).with(expected_ssh_command_no_port, any_args).and_return(result) + + call_method + end + end + + context 'when port 80 specified in uri' do + let(:uri) { 'http://foo.com:80' } + + it 'does not provide port in ssh command' do + expect(ssh).to receive(:run).with(expected_ssh_command_no_port, any_args).and_return(result) + + call_method + end + end + + context 'when other port is specified in uri' do + let(:port) { 1234 } + let(:uri) { "http://foo.com:#{port}" } + + it "provides other port in ssh command" do + expect(ssh).to receive(:run).with(expected_ssh_command_port, any_args).and_return(result) + + call_method + end + end + end + + describe '#setup' do + let(:expected_ssh_command_no_port) { "ssh-keyscan -H foo.com >> #{known_hosts_file.path}" } + let(:expected_ssh_command_port) { "ssh-keyscan -H -p #{port} foo.com >> #{known_hosts_file.path}" } + let(:call_method) { ssh.setup } + + before do + allow(File).to receive(:binwrite).with(private_key_file, key.private_key) + allow(File).to receive(:chmod).with(0700, private_key_file) + end + + it_behaves_like 'providing correct ports' + end + + describe '#reset_2fa_codes' do + let(:expected_ssh_command_no_port) { "echo yes | ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path} git@foo.com 2fa_recovery_codes" } + let(:expected_ssh_command_port) { "echo yes | ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path} git@foo.com -p #{port} 2fa_recovery_codes" } + let(:call_method) { ssh.reset_2fa_codes } + + before do + allow(ssh).to receive(:git_user).and_return('git') + end + + it_behaves_like 'providing correct ports' + end + + describe '#git_user' do + context 'when running on CI' do + let(:uri) { 'http://gitlab.com' } + + before do + allow(QA::Runtime::Env).to receive(:running_in_ci?).and_return(true) + end + + it 'returns git user' do + expect(ssh.send(:git_user)).to eq('git') + end + end + + context 'when running against environment on a port other than 80 or 443' do + let(:uri) { 'http://localhost:3000' } + + before do + allow(Etc).to receive(:getlogin).and_return('dummy_username') + allow(QA::Runtime::Env).to receive(:running_in_ci?).and_return(false) + end + + it 'returns the local user' do + expect(ssh.send(:git_user)).to eq('dummy_username') + end + end + + context 'when running against environment on port 80 and not on CI (docker)' do + let(:uri) { 'http://localhost' } + + before do + allow(QA::Runtime::Env).to receive(:running_in_ci?).and_return(false) + end + + it 'returns git user' do + expect(ssh.send(:git_user)).to eq('git') + end + end + end +end diff --git a/qa/spec/support/wait_for_requests_spec.rb b/qa/spec/support/wait_for_requests_spec.rb index 79ee3eb5099..47c35addd9f 100644 --- a/qa/spec/support/wait_for_requests_spec.rb +++ b/qa/spec/support/wait_for_requests_spec.rb @@ -1,9 +1,8 @@ # frozen_string_literal: true -describe QA::Support::WaitForRequests do +RSpec.describe QA::Support::WaitForRequests do describe '.wait_for_requests' do before do - allow(subject).to receive(:finished_all_axios_requests?).and_return(true) allow(subject).to receive(:finished_all_ajax_requests?).and_return(true) allow(subject).to receive(:finished_loading?).and_return(true) end diff --git a/qa/spec/support/waiter_spec.rb b/qa/spec/support/waiter_spec.rb index 35f1e01289a..5b0c2c95d0d 100644 --- a/qa/spec/support/waiter_spec.rb +++ b/qa/spec/support/waiter_spec.rb @@ -2,7 +2,7 @@ require 'logger' -describe QA::Support::Waiter do +RSpec.describe QA::Support::Waiter do before do logger = ::Logger.new $stdout logger.level = ::Logger::DEBUG |