diff options
Diffstat (limited to 'qa/qa')
103 files changed, 1859 insertions, 596 deletions
diff --git a/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml b/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml index 3e83c8f0f77..052ba1c14fb 100644 --- a/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml +++ b/qa/qa/fixtures/monitored_auto_devops/.gitlab-ci.yml @@ -22,10 +22,12 @@ variables: stages: - production -# This job continuously deploys to staging/production on every push to `master`. +# This job continuously deploys to production on every push to `master`. production: stage: production + tags: + - qa script: - check_kube_domain - install_dependencies @@ -34,7 +36,6 @@ production: - initialize_tiller - create_secret - deploy - - persist_environment_url environment: name: production url: http://$CI_PROJECT_PATH_SLUG.$AUTO_DEVOPS_DOMAIN diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 771f135a95c..bd2fbbd80cb 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -12,6 +12,8 @@ module QA module Git class Repository include Scenario::Actable + include Support::Repeater + RepositoryCommandError = Class.new(StandardError) attr_writer :use_lfs, :gpg_key_id @@ -58,8 +60,8 @@ module QA end def clone(opts = '') - clone_result = run("git clone #{opts} #{uri} ./") - return clone_result.response unless clone_result.success + clone_result = run("git clone #{opts} #{uri} ./", max_attempts: 3) + return clone_result.response unless clone_result.success? enable_lfs_result = enable_lfs if use_lfs? @@ -92,7 +94,7 @@ module QA if use_lfs? git_lfs_track_result = run(%Q{git lfs track #{name} --lockable}) - return git_lfs_track_result.response unless git_lfs_track_result.success + return git_lfs_track_result.response unless git_lfs_track_result.success? end git_add_result = run(%Q{git add #{name}}) @@ -101,11 +103,11 @@ module QA end def delete_tag(tag_name) - run(%Q{git push origin --delete #{tag_name}}).to_s + run(%Q{git push origin --delete #{tag_name}}, max_attempts: 3).to_s end def commit(message) - run(%Q{git commit -m "#{message}"}).to_s + run(%Q{git commit -m "#{message}"}, max_attempts: 3).to_s end def commit_with_gpg(message) @@ -113,13 +115,21 @@ module QA end def push_changes(branch = 'master') - run("git push #{uri} #{branch}").to_s + run("git push #{uri} #{branch}", max_attempts: 3).to_s end def merge(branch) run("git merge #{branch}") end + def init_repository + run("git init") + end + + def pull(repository = nil, branch = nil) + run(['git', 'pull', repository, branch].compact.join(' ')) + end + def commits run('git log --oneline').to_s.split("\n") end @@ -164,8 +174,8 @@ module QA 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 - output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1") - output.response[/git< version (\d+)/, 1] || 'unknown' + result = run("git ls-remote #{uri}", env: "GIT_TRACE_PACKET=1", max_attempts: 3) + result.response[/git< version (\d+)/, 1] || 'unknown' end def try_add_credentials_to_netrc @@ -175,6 +185,10 @@ module QA save_netrc_content end + def file_content(file) + run("cat #{file}").to_s + end + private attr_reader :uri, :username, :password, :known_hosts_file, @@ -182,9 +196,12 @@ module QA alias_method :use_lfs?, :use_lfs - Result = Struct.new(:success, :response) do - alias_method :success?, :success + Result = Struct.new(:command, :exitstatus, :response) do alias_method :to_s, :response + + def success? + exitstatus.zero? + end end def add_credentials? @@ -209,19 +226,26 @@ module QA touch_gitconfig_result.to_s + git_lfs_install_result.to_s end - def run(command_str, *extra_env) - command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ') - Runtime::Logger.debug "Git: pwd=[#{Dir.pwd}], command=[#{command}]" + def run(command_str, env: [], max_attempts: 1) + command = [env_vars, *env, command_str, '2>&1'].compact.join(' ') + result = nil - output, status = Open3.capture2e(command) - output.chomp! - Runtime::Logger.debug "Git: output=[#{output}], exitstatus=[#{status.exitstatus}]" + 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 status.success? - raise RepositoryCommandError, "The command #{command} failed (#{status.exitstatus}) with the following output:\n#{output}" + unless result.success? + raise RepositoryCommandError, "The command #{result.command} failed (#{result.exitstatus}) with the following output:\n#{result.response}" end - Result.new(status.exitstatus == 0, output) + result end def default_credentials diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 42208f05c89..cb3827f8eb1 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -133,8 +133,13 @@ module QA end # replace with (..., page = self.class) - def click_element(name, page = nil, text: nil, wait: Capybara.default_max_wait_time) - find_element(name, text: text, wait: wait).click + def click_element(name, page = nil, **kwargs) + wait_for_requests + + wait = kwargs.delete(:wait) || Capybara.default_max_wait_time + text = kwargs.delete(:text) + + find(element_selector_css(name, kwargs), text: text, wait: wait).click page.validate_elements_present! if page end diff --git a/qa/qa/page/component/breadcrumbs.rb b/qa/qa/page/component/breadcrumbs.rb index 656aa380bbd..2576e376e4e 100644 --- a/qa/qa/page/component/breadcrumbs.rb +++ b/qa/qa/page/component/breadcrumbs.rb @@ -4,7 +4,11 @@ module QA module Page module Component module Breadcrumbs + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/layouts/nav/_breadcrumbs.html.haml' do element :breadcrumb_links_content end diff --git a/qa/qa/page/component/ci_badge_link.rb b/qa/qa/page/component/ci_badge_link.rb index 3db675c3a60..8629399e911 100644 --- a/qa/qa/page/component/ci_badge_link.rb +++ b/qa/qa/page/component/ci_badge_link.rb @@ -4,6 +4,8 @@ module QA module Page module Component module CiBadgeLink + extend QA::Page::PageConcern + COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running INCOMPLETE_STATUSES = %w[pending created running].freeze @@ -27,6 +29,8 @@ module QA end def self.included(base) + super + base.view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do element :status_badge end diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb index fbe19e5802b..a0aea6fe44d 100644 --- a/qa/qa/page/component/clone_panel.rb +++ b/qa/qa/page/component/clone_panel.rb @@ -4,7 +4,11 @@ module QA module Page module Component module ClonePanel + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/projects/buttons/_clone.html.haml' do element :clone_dropdown element :clone_options diff --git a/qa/qa/page/component/confirm_modal.rb b/qa/qa/page/component/confirm_modal.rb index 355e2783fb7..039640d207a 100644 --- a/qa/qa/page/component/confirm_modal.rb +++ b/qa/qa/page/component/confirm_modal.rb @@ -4,7 +4,11 @@ module QA module Page module Component module ConfirmModal + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/shared/_confirm_modal.html.haml' do element :confirm_modal element :confirm_input diff --git a/qa/qa/page/component/custom_metric.rb b/qa/qa/page/component/custom_metric.rb new file mode 100644 index 00000000000..094979f5e18 --- /dev/null +++ b/qa/qa/page/component/custom_metric.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module CustomMetric + extend QA::Page::PageConcern + + def self.included(base) + super + + base.view 'app/assets/javascripts/custom_metrics/components/custom_metrics_form_fields.vue' do + element :custom_metric_prometheus_title_field + element :custom_metric_prometheus_query_field + element :custom_metric_prometheus_y_label_field + element :custom_metric_prometheus_unit_label_field + element :custom_metric_prometheus_legend_label_field + end + end + + def add_custom_metric + fill_element :custom_metric_prometheus_title_field, 'HTTP Requests Total' + fill_element :custom_metric_prometheus_query_field, 'rate(http_requests_total[5m])' + fill_element :custom_metric_prometheus_y_label_field, 'Requests/second' + fill_element :custom_metric_prometheus_unit_label_field, 'req/sec' + fill_element :custom_metric_prometheus_legend_label_field, 'HTTP requests' + + save_changes + end + + def save_changes + click_button(class: 'btn-success') + end + + def delete_custom_metric + click_button(class: 'btn-danger') + within('.modal-content') { click_button(class: 'btn-danger') } + end + + def edit_custom_metric + fill_element :custom_metric_prometheus_title_field, '' + fill_element :custom_metric_prometheus_title_field, 'Throughput' + + save_changes + end + end + end + end +end diff --git a/qa/qa/page/component/design_management.rb b/qa/qa/page/component/design_management.rb new file mode 100644 index 00000000000..a8a24bd3949 --- /dev/null +++ b/qa/qa/page/component/design_management.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module DesignManagement + extend QA::Page::PageConcern + + def self.included(base) + super + + base.class_eval do + view 'app/assets/javascripts/design_management/components/design_notes/design_discussion.vue' do + element :design_discussion_content + end + + view 'app/assets/javascripts/design_management/components/design_notes/design_note.vue' do + element :note_content + end + + view 'app/assets/javascripts/design_management/components/design_notes/design_reply_form.vue' do + element :note_textarea + element :save_comment_button + end + + view 'app/assets/javascripts/design_management/components/design_overlay.vue' do + element :design_image_button + end + + view 'app/assets/javascripts/design_management/components/list/item.vue' do + element :design_file_name + element :design_image + end + end + end + + def add_annotation(note) + click_element(:design_image_button) + fill_element(:note_textarea, note) + click_element(:save_comment_button) + + # It takes a moment for the annotation to be saved. + # We'll check for the annotation in a test, but here we'll at least + # wait for the "Save comment" button to disappear + saved = has_no_element?(:save_comment_button) + + raise ExpectationNotMet, %q(There was a problem while adding the annotation) unless saved + end + + def add_design(design_file_path) + # `attach_file` doesn't seem able to find element via data attributes. + # 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' }) + + filename = ::File.basename(design_file_path) + + found = wait_until(reload: false, sleep_interval: 1) do + image = find_element(:design_image) + + has_element?(:design_file_name, text: filename) && + image["complete"] && + image["naturalWidth"].to_i > 0 + end + + raise ElementNotFound, %Q(Attempted to attach design "#{filename}" but it did not appear) unless found + end + + def click_design(filename) + click_element(:design_file_name, text: filename) + end + + def has_annotation?(note) + within_element_by_index(:design_discussion_content, 0) do + has_element?(:note_content, text: note) + end + end + end + end + end +end diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb index 7eb1257db71..f82bb81a3fc 100644 --- a/qa/qa/page/component/groups_filter.rb +++ b/qa/qa/page/component/groups_filter.rb @@ -4,7 +4,11 @@ module QA module Page module Component module GroupsFilter + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/shared/groups/_search_form.html.haml' do element :groups_filter end diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb index 1155d4da036..bbab1746d7a 100644 --- a/qa/qa/page/component/issuable/common.rb +++ b/qa/qa/page/component/issuable/common.rb @@ -5,7 +5,11 @@ module QA module Component module Issuable module Common + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/assets/javascripts/issue_show/components/title.vue' do element :edit_button element :title, required: true diff --git a/qa/qa/page/component/lazy_loader.rb b/qa/qa/page/component/lazy_loader.rb index 6f74a4691ba..2123431fc55 100644 --- a/qa/qa/page/component/lazy_loader.rb +++ b/qa/qa/page/component/lazy_loader.rb @@ -4,7 +4,11 @@ module QA module Page module Component module LazyLoader + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/assets/javascripts/lazy_loader.js' do element :js_lazy_loaded end diff --git a/qa/qa/page/component/legacy_clone_panel.rb b/qa/qa/page/component/legacy_clone_panel.rb index 7b4b30623a6..ebab9fd708c 100644 --- a/qa/qa/page/component/legacy_clone_panel.rb +++ b/qa/qa/page/component/legacy_clone_panel.rb @@ -4,7 +4,11 @@ module QA module Page module Component module LegacyClonePanel + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/shared/_clone_panel.html.haml' do element :clone_dropdown element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb index 3e8ed9069ce..0e9cdd49519 100644 --- a/qa/qa/page/component/note.rb +++ b/qa/qa/page/component/note.rb @@ -4,7 +4,11 @@ module QA module Page module Component module Note + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/assets/javascripts/notes/components/comment_form.vue' do element :note_dropdown element :discussion_option diff --git a/qa/qa/page/component/project/templates.rb b/qa/qa/page/component/project/templates.rb new file mode 100644 index 00000000000..8baf15acdff --- /dev/null +++ b/qa/qa/page/component/project/templates.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module QA + module Page::Component + module Project + module Templates + def use_template_for_project(project_name) + within find_element(:template_option_row, text: project_name) do + click_element :use_template_button + end + end + end + end + end +end diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb index e667fad1dd3..b8beb64b6bd 100644 --- a/qa/qa/page/component/select2.rb +++ b/qa/qa/page/component/select2.rb @@ -18,10 +18,14 @@ module QA end end - def search_and_select(item_text) + def search_item(item_text) find('.select2-input').set(item_text) wait_for_search_to_complete + end + + def search_and_select(item_text) + search_item(item_text) select_item(item_text) end @@ -36,7 +40,7 @@ module QA end def dropdown_open? - has_css?('.select2-input') + find('.select2-focusser').disabled? end end end diff --git a/qa/qa/page/component/web_ide/alert.rb b/qa/qa/page/component/web_ide/alert.rb index 0f0623d5ebf..c2903662b52 100644 --- a/qa/qa/page/component/web_ide/alert.rb +++ b/qa/qa/page/component/web_ide/alert.rb @@ -5,8 +5,12 @@ module QA module Component module WebIDE module Alert - def self.prepended(page) - page.module_eval do + extend QA::Page::PageConcern + + def self.prepended(base) + super + + base.class_eval do view 'app/assets/javascripts/ide/components/error_message.vue' do element :flash_alert end diff --git a/qa/qa/page/dashboard/snippet/edit.rb b/qa/qa/page/dashboard/snippet/edit.rb new file mode 100644 index 00000000000..d28b8178c99 --- /dev/null +++ b/qa/qa/page/dashboard/snippet/edit.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Page + 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 + 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 + + def save_changes + click_element(:submit_button) + wait_until { assert_no_element(:submit_button) } + end + + private + + def text_area + find('#editor textarea', visible: false) + end + end + end + end + end +end diff --git a/qa/qa/page/dashboard/snippet/new.rb b/qa/qa/page/dashboard/snippet/new.rb index da5013e787e..d1a194ba1db 100644 --- a/qa/qa/page/dashboard/snippet/new.rb +++ b/qa/qa/page/dashboard/snippet/new.rb @@ -5,16 +5,33 @@ module QA module Dashboard module Snippet class New < Page::Base + view 'app/assets/javascripts/snippets/components/edit.vue' do + element :submit_button + end + + view 'app/assets/javascripts/snippets/components/snippet_description_edit.vue' do + element :snippet_description_field + element :description_placeholder, required: true + end + + view 'app/assets/javascripts/snippets/components/snippet_title.vue' do + element :snippet_title, required: true + end + + view 'app/assets/javascripts/snippets/components/snippet_blob_edit.vue' do + element :snippet_file_name + end + view 'app/views/shared/form_elements/_description.html.haml' do element :issuable_form_description end view 'app/views/shared/snippets/_form.html.haml' do - element :description_field + element :snippet_description_field element :description_placeholder element :snippet_title element :snippet_file_name - element :create_snippet_button + element :submit_button end view 'app/views/projects/_zen.html.haml' do @@ -28,7 +45,7 @@ module QA def fill_description(description) click_element :description_placeholder - fill_element :description_field, description + fill_element :snippet_description_field, description end def set_visibility(visibility) @@ -46,7 +63,8 @@ module QA end def click_create_snippet_button - click_element :create_snippet_button + wait_until(reload: false) { !find_element(:submit_button).disabled? } + click_element :submit_button end private diff --git a/qa/qa/page/dashboard/snippet/show.rb b/qa/qa/page/dashboard/snippet/show.rb index 88d6ef02d22..d43b64cd1d4 100644 --- a/qa/qa/page/dashboard/snippet/show.rb +++ b/qa/qa/page/dashboard/snippet/show.rb @@ -5,10 +5,17 @@ module QA module Dashboard module Snippet class Show < Page::Base + view 'app/assets/javascripts/snippets/components/snippet_description_view.vue' do + element :snippet_description_field + end + + view 'app/assets/javascripts/snippets/components/snippet_title.vue' do + element :snippet_title, required: true + end + view 'app/views/shared/snippets/_header.html.haml' do element :snippet_title, required: true - element :snippet_description, required: true - element :embed_type + element :snippet_description_field, required: true element :snippet_box end @@ -16,22 +23,38 @@ module QA element :file_title_name end + view 'app/assets/javascripts/blob/components/blob_header_filepath.vue' do + element :file_title_name + end + view 'app/views/shared/_file_highlight.html.haml' do element :file_content end + view 'app/assets/javascripts/vue_shared/components/blob_viewers/simple_viewer.vue' do + element :file_content + end + + view 'app/assets/javascripts/snippets/components/snippet_header.vue' do + element :snippet_action_button + element :delete_snippet_button + end + + view 'app/assets/javascripts/snippets/components/snippet_blob_view.vue' do + element :clone_button + end + + view 'app/assets/javascripts/vue_shared/components/clone_dropdown.vue' do + element :copy_http_url_button + element :copy_ssh_url_button + end + def has_snippet_title?(snippet_title) has_element? :snippet_title, text: snippet_title end def has_snippet_description?(snippet_description) - has_element? :snippet_description, text: snippet_description - end - - def has_embed_type?(embed_type) - within_element(:embed_type) do - has_text?(embed_type) - end + has_element? :snippet_description_field, text: snippet_description end def has_visibility_type?(visibility_type) @@ -52,6 +75,30 @@ module QA has_text?(file_content) end end + + def click_edit_button + finished_loading? + click_element(:snippet_action_button, action: 'Edit') + end + + def click_delete_button + finished_loading? + click_element(:snippet_action_button, action: 'Delete') + click_element(:delete_snippet_button) + finished_loading? # wait for the page to reload after deletion + end + + def get_repository_uri_http + finished_loading? + click_element(:clone_button) + Git::Location.new(find_element(:copy_http_url_button)['data-clipboard-text']).uri.to_s + end + + def get_repository_uri_ssh + finished_loading? + click_element(:clone_button) + Git::Location.new(find_element(:copy_ssh_url_button)['data-clipboard-text']).uri.to_s + end end end end diff --git a/qa/qa/page/file/shared/commit_button.rb b/qa/qa/page/file/shared/commit_button.rb index 9ea4f4e7818..c1c5907f27d 100644 --- a/qa/qa/page/file/shared/commit_button.rb +++ b/qa/qa/page/file/shared/commit_button.rb @@ -5,7 +5,11 @@ module QA module File module Shared module CommitButton + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/projects/_commit_button.html.haml' do element :commit_button end diff --git a/qa/qa/page/file/shared/commit_message.rb b/qa/qa/page/file/shared/commit_message.rb index ce3b1e9939c..823ce7bf7f9 100644 --- a/qa/qa/page/file/shared/commit_message.rb +++ b/qa/qa/page/file/shared/commit_message.rb @@ -5,7 +5,11 @@ module QA module File module Shared module CommitMessage + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/shared/_commit_message_container.html.haml' do element :commit_message, "text_area_tag 'commit_message'" # rubocop:disable QA/ElementWithPattern end diff --git a/qa/qa/page/file/shared/editor.rb b/qa/qa/page/file/shared/editor.rb index 448c09cfbca..ce4465d2a5c 100644 --- a/qa/qa/page/file/shared/editor.rb +++ b/qa/qa/page/file/shared/editor.rb @@ -5,7 +5,11 @@ module QA module File module Shared module Editor + extend QA::Page::PageConcern + def self.included(base) + super + base.view 'app/views/projects/blob/_editor.html.haml' do element :editor end diff --git a/qa/qa/page/group/sub_menus/common.rb b/qa/qa/page/group/sub_menus/common.rb index 96efc8da98d..86102f70d29 100644 --- a/qa/qa/page/group/sub_menus/common.rb +++ b/qa/qa/page/group/sub_menus/common.rb @@ -5,9 +5,12 @@ module QA module Group module SubMenus module Common + extend QA::Page::PageConcern include QA::Page::SubMenus::Common def self.included(base) + super + base.class_eval do view 'app/views/layouts/nav/sidebar/_group.html.haml' do element :group_sidebar diff --git a/qa/qa/page/main/terms.rb b/qa/qa/page/main/terms.rb index a4928f24397..a0de267fb31 100644 --- a/qa/qa/page/main/terms.rb +++ b/qa/qa/page/main/terms.rb @@ -1,20 +1,22 @@ # frozen_string_literal: true module QA - module Page::Main - class Terms < Page::Base - view 'app/views/layouts/terms.html.haml' do - element :user_avatar, required: true - end + module Page + module Main + class Terms < Page::Base + view 'app/views/layouts/terms.html.haml' do + element :user_avatar, required: true + end - view 'app/views/users/terms/index.html.haml' do - element :terms_content, required: true + view 'app/views/users/terms/index.html.haml' do + element :terms_content, required: true - element :accept_terms_button - end + element :accept_terms_button + end - def accept_terms - click_element :accept_terms_button, Page::Main::Menu + def accept_terms + click_element :accept_terms_button, Page::Main::Menu + end end end end diff --git a/qa/qa/page/page_concern.rb b/qa/qa/page/page_concern.rb new file mode 100644 index 00000000000..6ba2d27f574 --- /dev/null +++ b/qa/qa/page/page_concern.rb @@ -0,0 +1,16 @@ +module QA + module Page + module PageConcern + def included(base) + unless base.is_a?(Class) + raise "Expected #{self} to be prepended to a class, but #{base} is a module!" + end + + unless base.ancestors.include?(::QA::Page::Base) + raise "Expected #{self} to be prepended to a class that inherits from ::QA::Page::Base, but #{base} doesn't!" + end + end + alias_method :prepended, :included + end + end +end diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb index 7069e7d3e4f..fd191fa3e27 100644 --- a/qa/qa/page/profile/personal_access_tokens.rb +++ b/qa/qa/page/profile/personal_access_tokens.rb @@ -6,9 +6,9 @@ module QA module Page module Profile class PersonalAccessTokens < Page::Base - view 'app/views/shared/_personal_access_tokens_form.html.haml' do + view 'app/views/shared/access_tokens/_form.html.haml' do element :expiry_date_field - element :personal_access_token_name_field + element :access_token_name_field element :create_token_button end @@ -16,15 +16,15 @@ module QA element :api_radio, 'qa-#{scope}-radio' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck end - view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do - element :created_personal_access_token + view 'app/views/shared/access_tokens/_created_container.html.haml' do + element :created_access_token end - view 'app/views/shared/_personal_access_tokens_table.html.haml' do + view 'app/views/shared/access_tokens/_table.html.haml' do element :revoke_button end def fill_token_name(name) - fill_element(:personal_access_token_name_field, name) + fill_element(:access_token_name_field, name) end def check_api @@ -36,7 +36,7 @@ module QA end def created_access_token - find_element(:created_personal_access_token, wait: 30).value + find_element(:created_access_token, wait: 30).value end def fill_expiry_date(date) diff --git a/qa/qa/page/profile/ssh_keys.rb b/qa/qa/page/profile/ssh_keys.rb index 082202f91ca..810877e21ad 100644 --- a/qa/qa/page/profile/ssh_keys.rb +++ b/qa/qa/page/profile/ssh_keys.rb @@ -5,6 +5,7 @@ module QA module Profile class SSHKeys < Page::Base view 'app/views/profiles/keys/_form.html.haml' do + element :key_expiry_date_field element :key_title_field element :key_public_key_field element :add_key_button @@ -19,17 +20,26 @@ module QA end def add_key(public_key, title) - fill_element :key_public_key_field, public_key - fill_element :key_title_field, title + fill_element(:key_public_key_field, public_key) + fill_element(:key_title_field, title) + # Expire in 2 days just in case the key is created just before midnight + fill_expiry_date(Date.today + 2) - click_element :add_key_button + click_element(:add_key_button) + end + + def fill_expiry_date(date) + date = date.strftime('%m/%d/%Y') if date.is_a?(Date) + Date.strptime(date, '%m/%d/%Y') rescue ArgumentError raise "Expiry date must be in mm/dd/yyyy format" + + fill_element(:key_expiry_date_field, date) end def remove_key(title) click_link(title) accept_alert do - click_element :delete_key_button + click_element(:delete_key_button) end end diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index b5ad63ab8de..ace2537fc0e 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -9,6 +9,15 @@ module QA element :assignee_link end + view 'app/views/projects/issues/export_csv/_button.html.haml' do + element :export_as_csv_button + end + + view 'app/views/projects/issues/export_csv/_modal.html.haml' do + element :export_issues_button + element :export_issues_modal + end + view 'app/views/projects/issues/_issue.html.haml' do element :issue element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern @@ -34,6 +43,18 @@ module QA click_element :closed_issues_link end + def click_export_as_csv_button + click_element(:export_as_csv_button) + end + + def click_export_issues_button + click_element(:export_issues_button) + end + + def export_issues_modal + find_element(:export_issues_modal) + end + def has_assignee_link_count?(count) all_elements(:assignee_link, count: count) end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 8365ecb6348..dd74ff28763 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -7,6 +7,7 @@ module QA class Show < Page::Base include Page::Component::Issuable::Common include Page::Component::Note + include Page::Component::DesignManagement view 'app/assets/javascripts/notes/components/comment_form.vue' do element :comment_button @@ -56,6 +57,23 @@ module QA element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern end + view 'app/views/projects/issues/_tabs.html.haml' do + element :discussion_tab_link + element :discussion_tab_content + element :designs_tab_link + element :designs_tab_content + end + + def click_discussion_tab + click_element(:discussion_tab_link) + active_element?(:discussion_tab_content) + end + + def click_designs_tab + click_element(:designs_tab_link) + active_element?(:designs_tab_content) + end + def click_milestone_link click_element(:milestone_link) end diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 26db2f20c1b..971b8c5e5f8 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -1,51 +1,55 @@ # frozen_string_literal: true -module QA::Page - module Project::Job - class Show < QA::Page::Base - include Component::CiBadgeLink +module QA + module Page + module Project + module Job + class Show < QA::Page::Base + include Component::CiBadgeLink - view 'app/assets/javascripts/jobs/components/log/log.vue' do - element :job_log_content - end + view 'app/assets/javascripts/jobs/components/log/log.vue' do + element :job_log_content + end - view 'app/assets/javascripts/jobs/components/stages_dropdown.vue' do - element :pipeline_path - end + view 'app/assets/javascripts/jobs/components/stages_dropdown.vue' do + element :pipeline_path + end - view 'app/assets/javascripts/jobs/components/sidebar.vue' do - element :retry_button - end + view 'app/assets/javascripts/jobs/components/sidebar.vue' do + element :retry_button + end - def successful?(timeout: 60) - raise "Timed out waiting for the build trace to load" unless loaded? - raise "Timed out waiting for the status to be a valid completed state" unless completed?(timeout: timeout) + def successful?(timeout: 60) + raise "Timed out waiting for the build trace to load" unless loaded? + raise "Timed out waiting for the status to be a valid completed state" unless completed?(timeout: timeout) - passed? - end + passed? + end - # Reminder: You may wish to wait for a particular job status before checking output - def output(wait: 5) - result = '' + # Reminder: You may wish to wait for a particular job status before checking output + def output(wait: 5) + result = '' - wait_until(reload: false, max_duration: wait, sleep_interval: 1) do - result = find_element(:job_log_content).text + wait_until(reload: false, max_duration: wait, sleep_interval: 1) do + result = find_element(:job_log_content).text - result.include?('Job') - end + result.include?('Job') + end - result - end + result + end - def retry! - click_element :retry_button - end + def retry! + click_element :retry_button + end - private + private - def loaded?(wait: 60) - wait_until(reload: true, max_duration: wait, sleep_interval: 1) do - has_element?(:job_log_content, wait: 1) + def loaded?(wait: 60) + wait_until(reload: true, max_duration: wait, sleep_interval: 1) do + has_element?(:job_log_content, wait: 1) + end + end end end end diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 97214e22820..f6c015f64ea 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -5,6 +5,7 @@ module QA module Project class New < Page::Base include Page::Component::Select2 + include Page::Component::Project::Templates view 'app/views/projects/new.html.haml' do element :project_create_from_template_tab @@ -26,6 +27,11 @@ module QA element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern end + view 'app/views/projects/project_templates/_built_in_templates.html.haml' do + element :use_template_button + element :template_option_row + end + def choose_test_namespace choose_namespace(Runtime::Namespace.path) end diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb index 84b58e9ea5b..0c92f9a9f28 100644 --- a/qa/qa/page/project/operations/kubernetes/index.rb +++ b/qa/qa/page/project/operations/kubernetes/index.rb @@ -17,6 +17,10 @@ module QA def has_cluster?(cluster) has_element?(:cluster, cluster_name: cluster.to_s) end + + def click_on_cluster(cluster) + click_on cluster.cluster_name + end end end end diff --git a/qa/qa/page/project/operations/metrics/show.rb b/qa/qa/page/project/operations/metrics/show.rb index 020a3a1d5f8..2228cca1d3d 100644 --- a/qa/qa/page/project/operations/metrics/show.rb +++ b/qa/qa/page/project/operations/metrics/show.rb @@ -14,17 +14,22 @@ module QA element :dashboards_filter_dropdown element :environments_dropdown element :edit_dashboard_button - element :show_last_dropdown + element :range_picker_dropdown end view 'app/assets/javascripts/monitoring/components/duplicate_dashboard_form.vue' do element :duplicate_dashboard_filename_field end - view 'app/assets/javascripts/monitoring/components/panel_type.vue' do + view 'app/assets/javascripts/monitoring/components/dashboard_panel.vue' do element :prometheus_graph_widgets element :prometheus_widgets_dropdown element :alert_widget_menu_item + element :generate_chart_link_menu_item + end + + view 'app/assets/javascripts/vue_shared/components/date_time_picker/date_time_picker.vue' do + element :quick_range_item end def wait_for_metrics @@ -66,9 +71,18 @@ module QA end def show_last(range = '8 hours') - click_element :show_last_dropdown - within_element :show_last_dropdown do - click_on range + all_elements(:range_picker_dropdown, minimum: 1).first.click + click_element :quick_range_item, text: range + end + + def copy_link_to_first_chart + all_elements(:prometheus_widgets_dropdown, minimum: 1).first.click + find_element(:generate_chart_link_menu_item)['data-clipboard-text'] + end + + def has_custom_metric?(metric) + within_element :prometheus_graphs do + has_text?(metric) end end diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb index 327eedeaf91..54e4d0fb2fc 100644 --- a/qa/qa/page/project/pipeline/index.rb +++ b/qa/qa/page/project/pipeline/index.rb @@ -1,41 +1,45 @@ # frozen_string_literal: true -module QA::Page - module Project::Pipeline - class Index < QA::Page::Base - view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do - element :pipeline_url_link - end - - view 'app/assets/javascripts/pipelines/components/pipelines_table_row.vue' do - element :pipeline_commit_status - element :pipeline_retry_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 - - def wait_for_latest_pipeline_success - wait_for_latest_pipeline_status { has_text?('passed') } - end - - def wait_for_latest_pipeline_completion - wait_for_latest_pipeline_status { has_text?('passed') || has_text?('failed') } - end - - def wait_for_latest_pipeline_status - wait_until(reload: false, max_duration: 360) do - within_element_by_index(:pipeline_commit_status, 0) { yield } - end - end - - def wait_for_latest_pipeline_success_or_retry - wait_for_latest_pipeline_completion - - if has_text?('failed') - click_element :pipeline_retry_button - wait_for_latest_pipeline_success +module QA + module Page + module Project + module Pipeline + class Index < QA::Page::Base + view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do + element :pipeline_url_link + end + + view 'app/assets/javascripts/pipelines/components/pipelines_table_row.vue' do + element :pipeline_commit_status + element :pipeline_retry_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 + + def wait_for_latest_pipeline_success + wait_for_latest_pipeline_status { has_text?('passed') } + end + + def wait_for_latest_pipeline_completion + wait_for_latest_pipeline_status { has_text?('passed') || has_text?('failed') } + end + + def wait_for_latest_pipeline_status + wait_until(reload: false, max_duration: 360) do + within_element_by_index(:pipeline_commit_status, 0) { yield } + end + end + + def wait_for_latest_pipeline_success_or_retry + wait_for_latest_pipeline_completion + + if has_text?('failed') + click_element :pipeline_retry_button + wait_for_latest_pipeline_success + end + end end end end diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index 1003b828a32..d22dfefc096 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -1,73 +1,77 @@ # frozen_string_literal: true -module QA::Page - module Project::Pipeline - class Show < QA::Page::Base - 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 - end +module QA + module Page + module Project + module Pipeline + class Show < QA::Page::Base + 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 + end - view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do - element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern - end + view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do + element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern + 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_link - 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_link + end - view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do - element :linked_pipeline_button - end + view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do + element :linked_pipeline_button + end - view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do - element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern - end + view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do + element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern + end - view 'app/views/projects/pipelines/_info.html.haml' do - element :pipeline_badges - end + view 'app/views/projects/pipelines/_info.html.haml' do + element :pipeline_badges + end - def running?(wait: 0) - within('.ci-header-container') do - page.has_content?('running', wait: wait) - end - end + def running?(wait: 0) + within('.ci-header-container') 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 - has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact) + def has_build?(name, status: :success, wait: nil) + within('.pipeline-graph') do + within('.ci-job-component', text: name) do + has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact) + end + end end - end - end - def has_job?(job_name) - has_element?(:job_link, text: job_name) - end + def has_job?(job_name) + has_element?(:job_link, text: job_name) + end - def has_no_job?(job_name) - has_no_element?(:job_link, text: job_name) - end + def has_no_job?(job_name) + has_no_element?(:job_link, text: job_name) + end - def has_tag?(tag_name) - within_element(:pipeline_badges) do - has_selector?('.badge', text: tag_name) - end - end + def has_tag?(tag_name) + within_element(:pipeline_badges) do + has_selector?('.badge', text: tag_name) + end + end - def click_job(job_name) - click_element(:job_link, text: job_name) - end + def click_job(job_name) + click_element(:job_link, text: job_name) + end - def click_linked_job(project_name) - click_element(:linked_pipeline_button, text: /#{project_name}/) - end + def click_linked_job(project_name) + click_element(:linked_pipeline_button, text: /#{project_name}/) + end - def click_on_first_job - first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click + def click_on_first_job + first('.js-pipeline-graph-job-link', wait: QA::Support::Repeater::DEFAULT_MAX_WAIT_TIME).click + end + end end end end diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb index c95c47fa560..3bb5181a31c 100644 --- a/qa/qa/page/project/settings/advanced.rb +++ b/qa/qa/page/project/settings/advanced.rb @@ -57,6 +57,10 @@ module QA click_element :download_export_link end + def has_download_export_link? + has_element? :download_export_link + end + def archive_project page.accept_alert("Are you sure that you want to archive this project?") do click_element :archive_project_link diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb index 46f93fad61e..aa27c030b78 100644 --- a/qa/qa/page/project/settings/ci_cd.rb +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -5,12 +5,19 @@ module QA module Project module Settings class CICD < Page::Base - include Common + include QA::Page::Settings::Common view 'app/views/projects/settings/ci_cd/show.html.haml' do element :autodevops_settings_content element :runners_settings_content element :variables_settings_content + element :general_pipelines_settings_content + end + + def expand_general_pipelines(&block) + expand_section(:general_pipelines_settings_content) do + Settings::GeneralPipelines.perform(&block) + end end def expand_runners_settings(&block) diff --git a/qa/qa/page/project/settings/ci_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index 6cdf40cd1da..de268b14aa2 100644 --- a/qa/qa/page/project/settings/ci_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -5,7 +5,7 @@ module QA module Project module Settings class CiVariables < Page::Base - include Common + include QA::Page::Settings::Common view 'app/assets/javascripts/ci_variable_list/components/ci_variable_modal.vue' do element :ci_variable_key_field diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb deleted file mode 100644 index f5f22623060..00000000000 --- a/qa/qa/page/project/settings/common.rb +++ /dev/null @@ -1,13 +0,0 @@ -# frozen_string_literal: true - -module QA - module Page - module Project - module Settings - module Common - include QA::Page::Settings::Common - end - end - end - end -end diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index c330d090ce6..8d655b0684e 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -5,7 +5,7 @@ module QA module Project module Settings class DeployKeys < Page::Base - view 'app/views/projects/deploy_keys/_form.html.haml' do + view 'app/views/shared/deploy_keys/_form.html.haml' do element :deploy_key_title, 'text_field :title' # rubocop:disable QA/ElementWithPattern element :deploy_key_key, 'text_area :key' # rubocop:disable QA/ElementWithPattern end diff --git a/qa/qa/page/project/settings/general_pipelines.rb b/qa/qa/page/project/settings/general_pipelines.rb new file mode 100644 index 00000000000..5a98849a41d --- /dev/null +++ b/qa/qa/page/project/settings/general_pipelines.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class GeneralPipelines < Page::Base + include QA::Page::Settings::Common + + view 'app/views/projects/settings/ci_cd/_form.html.haml' do + element :build_coverage_regex_field + element :save_general_pipelines_changes_button + end + + def configure_coverage_regex(pattern) + fill_element :build_coverage_regex_field, pattern + click_element :save_general_pipelines_changes_button + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/incidents.rb b/qa/qa/page/project/settings/incidents.rb new file mode 100644 index 00000000000..94d5fc369ad --- /dev/null +++ b/qa/qa/page/project/settings/incidents.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class Incidents < Page::Base + view 'app/views/projects/settings/operations/_incidents.html.haml' do + element :create_issue_checkbox + element :incident_templates_dropdown + element :save_changes_button + end + + def enable_issues_for_incidents + check_element :create_issue_checkbox + end + + def select_issue_template(template) + within_element :incident_templates_dropdown do + find(:option, template).select_option + end + end + + def save_incident_settings + click_element :save_changes_button + end + + def has_template?(template) + within_element :incident_templates_dropdown do + has_text?(template) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/integrations.rb b/qa/qa/page/project/settings/integrations.rb new file mode 100644 index 00000000000..436a42fb093 --- /dev/null +++ b/qa/qa/page/project/settings/integrations.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class Integrations < QA::Page::Base + view 'app/views/shared/integrations/_index.html.haml' do + element :prometheus_link, '{ data: { qa_selector: "#{integration.to_param' # rubocop:disable QA/ElementWithPattern + end + + def click_on_prometheus_integration + click_element :prometheus_link + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index 18d55598d90..efae497b6ba 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -5,7 +5,7 @@ module QA module Project module Settings class Main < Page::Base - include Common + include QA::Page::Settings::Common include Component::Select2 include SubMenus::Project diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb index 7da2c9d168c..0092426b31f 100644 --- a/qa/qa/page/project/settings/merge_request.rb +++ b/qa/qa/page/project/settings/merge_request.rb @@ -5,7 +5,7 @@ module QA module Project module Settings class MergeRequest < QA::Page::Base - include Common + include QA::Page::Settings::Common view 'app/views/projects/edit.html.haml' do element :save_merge_request_changes diff --git a/qa/qa/page/project/settings/operations.rb b/qa/qa/page/project/settings/operations.rb new file mode 100644 index 00000000000..f6e005d3189 --- /dev/null +++ b/qa/qa/page/project/settings/operations.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class Operations < Page::Base + include QA::Page::Settings::Common + + view 'app/views/projects/settings/operations/_incidents.html.haml' do + element :incidents_settings_content + end + + def expand_incidents(&block) + expand_section(:incidents_settings_content) do + Settings::Incidents.perform(&block) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 8810b971fda..8e9a24a4741 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -5,7 +5,7 @@ module QA module Project module Settings class Repository < Page::Base - include Common + include QA::Page::Settings::Common view 'app/views/projects/protected_branches/shared/_index.html.haml' do element :protected_branches_settings @@ -19,7 +19,7 @@ module QA element :deploy_tokens_settings end - view 'app/views/projects/deploy_keys/_index.html.haml' do + view 'app/views/shared/deploy_keys/_index.html.haml' do element :deploy_keys_settings end diff --git a/qa/qa/page/project/settings/services/prometheus.rb b/qa/qa/page/project/settings/services/prometheus.rb new file mode 100644 index 00000000000..8ae4ded535e --- /dev/null +++ b/qa/qa/page/project/settings/services/prometheus.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + module Services + class Prometheus < Page::Base + include Page::Component::CustomMetric + + view 'app/views/projects/services/prometheus/_custom_metrics.html.haml' do + element :custom_metrics_container + element :new_metric_button + end + + def click_on_custom_metric(custom_metric) + within_element :custom_metrics_container do + click_on custom_metric + end + end + + def click_on_new_metric + click_element :new_metric_button + end + + def has_custom_metric?(custom_metric) + within_element :custom_metrics_container do + has_text? custom_metric + end + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/sub_menus/ci_cd.rb b/qa/qa/page/project/sub_menus/ci_cd.rb index 2f0bc8b9ba6..9405ea97fff 100644 --- a/qa/qa/page/project/sub_menus/ci_cd.rb +++ b/qa/qa/page/project/sub_menus/ci_cd.rb @@ -5,10 +5,14 @@ module QA module Project module SubMenus module CiCd - include Page::Project::SubMenus::Common + extend QA::Page::PageConcern def self.included(base) + super + base.class_eval do + include QA::Page::Project::SubMenus::Common + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :link_pipelines end diff --git a/qa/qa/page/project/sub_menus/common.rb b/qa/qa/page/project/sub_menus/common.rb index da759398cff..85bf932be4a 100644 --- a/qa/qa/page/project/sub_menus/common.rb +++ b/qa/qa/page/project/sub_menus/common.rb @@ -5,6 +5,7 @@ module QA module Project module SubMenus module Common + extend QA::Page::PageConcern include QA::Page::SubMenus::Common private diff --git a/qa/qa/page/project/sub_menus/issues.rb b/qa/qa/page/project/sub_menus/issues.rb index d27a250a300..c15a8ec4cc7 100644 --- a/qa/qa/page/project/sub_menus/issues.rb +++ b/qa/qa/page/project/sub_menus/issues.rb @@ -5,10 +5,14 @@ module QA module Project module SubMenus module Issues - include Page::Project::SubMenus::Common + extend QA::Page::PageConcern def self.included(base) + super + base.class_eval do + include QA::Page::Project::SubMenus::Common + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :issue_boards_link element :issues_item diff --git a/qa/qa/page/project/sub_menus/operations.rb b/qa/qa/page/project/sub_menus/operations.rb index bcbc1dc16d3..ff9c8a21174 100644 --- a/qa/qa/page/project/sub_menus/operations.rb +++ b/qa/qa/page/project/sub_menus/operations.rb @@ -5,12 +5,16 @@ module QA module Project module SubMenus module Operations - include Page::Project::SubMenus::Common + extend QA::Page::PageConcern def self.included(base) + super + base.class_eval do + include QA::Page::Project::SubMenus::Common + view 'app/views/layouts/nav/sidebar/_project.html.haml' do - element :link_operations + element :operations_link element :operations_environments_link element :operations_metrics_link end @@ -45,8 +49,8 @@ module QA def hover_operations within_sidebar do - scroll_to_element(:link_operations) - find_element(:link_operations).hover + scroll_to_element(:operations_link) + find_element(:operations_link).hover yield end diff --git a/qa/qa/page/project/sub_menus/project.rb b/qa/qa/page/project/sub_menus/project.rb index 6f1bc131f84..4af640301b9 100644 --- a/qa/qa/page/project/sub_menus/project.rb +++ b/qa/qa/page/project/sub_menus/project.rb @@ -5,10 +5,14 @@ module QA module Project module SubMenus module Project - include Common + extend QA::Page::PageConcern def self.included(base) + super + base.class_eval do + include QA::Page::Project::SubMenus::Common + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :project_link end diff --git a/qa/qa/page/project/sub_menus/repository.rb b/qa/qa/page/project/sub_menus/repository.rb index 65149e631f3..38d6b8e50f4 100644 --- a/qa/qa/page/project/sub_menus/repository.rb +++ b/qa/qa/page/project/sub_menus/repository.rb @@ -5,10 +5,14 @@ module QA module Project module SubMenus module Repository - include Page::Project::SubMenus::Common + extend QA::Page::PageConcern def self.included(base) + super + base.class_eval do + include QA::Page::Project::SubMenus::Common + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :project_menu_repo element :branches_link @@ -44,5 +48,3 @@ module QA end end end - -QA::Page::Project::SubMenus::Repository.prepend_if_ee('QA::EE::Page::Project::SubMenus::Repository') diff --git a/qa/qa/page/project/sub_menus/settings.rb b/qa/qa/page/project/sub_menus/settings.rb index 8be442ba35d..0dd4bd1817a 100644 --- a/qa/qa/page/project/sub_menus/settings.rb +++ b/qa/qa/page/project/sub_menus/settings.rb @@ -5,15 +5,20 @@ module QA module Project module SubMenus module Settings - include Page::Project::SubMenus::Common + extend QA::Page::PageConcern def self.included(base) + super + base.class_eval do + include QA::Page::Project::SubMenus::Common + view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item element :link_members_settings element :general_settings_link element :integrations_settings_link + element :operations_settings_link end end end @@ -64,6 +69,14 @@ module QA end end + def go_to_operations_settings + hover_settings do + within_submenu do + click_element :operations_settings_link + end + end + end + private def hover_settings diff --git a/qa/qa/page/search/results.rb b/qa/qa/page/search/results.rb index 85f1d224935..55477db8804 100644 --- a/qa/qa/page/search/results.rb +++ b/qa/qa/page/search/results.rb @@ -1,53 +1,55 @@ # frozen_string_literal: true -module QA::Page - module Search - class Results < QA::Page::Base - view 'app/views/search/_category.html.haml' do - element :code_tab - element :projects_tab - end +module QA + module Page + module Search + class Results < QA::Page::Base + view 'app/views/search/_category.html.haml' do + element :code_tab + element :projects_tab + end - view 'app/views/search/results/_blob_data.html.haml' do - element :result_item_content - element :file_title_content - element :file_text_content - end + view 'app/views/search/results/_blob_data.html.haml' do + element :result_item_content + element :file_title_content + element :file_text_content + end - view 'app/views/shared/projects/_project.html.haml' do - element :project - end + view 'app/views/shared/projects/_project.html.haml' do + element :project + end - def switch_to_code - switch_to_tab(:code_tab) - end + def switch_to_code + switch_to_tab(:code_tab) + end - def switch_to_projects - switch_to_tab(:projects_tab) - end + def switch_to_projects + switch_to_tab(:projects_tab) + end - def has_file_in_project?(file_name, project_name) - has_element?(:result_item_content, text: "#{project_name}: #{file_name}") - end + def has_file_in_project?(file_name, project_name) + has_element?(:result_item_content, text: "#{project_name}: #{file_name}") + end - def has_file_with_content?(file_name, file_text) - within_element_by_index(:result_item_content, 0) do - break false unless has_element?(:file_title_content, text: file_name) + def has_file_with_content?(file_name, file_text) + within_element_by_index(:result_item_content, 0) do + break false unless has_element?(:file_title_content, text: file_name) - has_element?(:file_text_content, text: file_text) + has_element?(:file_text_content, text: file_text) + end end - end - def has_project?(project_name) - has_element?(:project, project_name: project_name) - end + def has_project?(project_name) + has_element?(:project, project_name: project_name) + end - private + private - def switch_to_tab(tab) - retry_until do - click_element(tab) - has_active_element?(tab) + def switch_to_tab(tab) + retry_until do + click_element(tab) + has_active_element?(tab) + end end end end diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index cac58c599ea..591aa449219 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -14,6 +14,7 @@ module QA ResourceQueryError = Class.new(RuntimeError) ResourceUpdateFailedError = Class.new(RuntimeError) ResourceURLMissingError = Class.new(RuntimeError) + InternalServerError = Class.new(RuntimeError) attr_reader :api_resource, :api_response attr_writer :api_client @@ -54,11 +55,23 @@ module QA end end - private - include Support::Api attr_writer :api_resource, :api_response + def api_put(body = api_put_body) + response = put( + Runtime::API::Request.new(api_client, api_put_path).url, + body) + + unless response.code == HTTP_STATUS_OK + raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`." + end + + process_api_response(parse_body(response)) + end + + private + def resource_web_url(resource) resource.fetch(:web_url) do raise ResourceURLMissingError, "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`." @@ -73,7 +86,9 @@ module QA url = Runtime::API::Request.new(api_client, get_path).url response = get(url) - unless response.code == HTTP_STATUS_OK + if response.code == HTTP_STATUS_SERVER_ERROR + raise InternalServerError, "Failed to GET #{url} - (#{response.code}): `#{response}`." + elsif response.code != HTTP_STATUS_OK raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`." end @@ -92,18 +107,6 @@ module QA process_api_response(parse_body(response)) end - def api_put - response = put( - Runtime::API::Request.new(api_client, api_put_path).url, - api_put_body) - - unless response.code == HTTP_STATUS_OK - raise ResourceFabricationFailedError, "Updating #{self.class.name} using the API failed (#{response.code}) with `#{response}`." - end - - process_api_response(parse_body(response)) - end - def api_delete url = Runtime::API::Request.new(api_client, api_delete_path).url response = delete(url) diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb deleted file mode 100644 index 7306acfe2a4..00000000000 --- a/qa/qa/resource/kubernetes_cluster.rb +++ /dev/null @@ -1,68 +0,0 @@ -# frozen_string_literal: true - -require 'securerandom' - -module QA - module Resource - class KubernetesCluster < Base - attr_writer :project, :cluster, - :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain - - attribute :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) - end - - def fabricate! - @project.visit! - - Page::Project::Menu.perform( - &:go_to_operations_kubernetes) - - Page::Project::Operations::Kubernetes::Index.perform( - &:add_kubernetes_cluster) - - Page::Project::Operations::Kubernetes::Add.perform( - &:add_existing_cluster) - - Page::Project::Operations::Kubernetes::AddExisting.perform do |cluster_page| - cluster_page.set_cluster_name(@cluster.cluster_name) - cluster_page.set_api_url(@cluster.api_url) - cluster_page.set_ca_certificate(@cluster.ca_certificate) - cluster_page.set_token(@cluster.token) - cluster_page.uncheck_rbac! unless @cluster.rbac - cluster_page.add_cluster! - end - - if @install_helm_tiller - Page::Project::Operations::Kubernetes::Show.perform do |show| - # We must wait a few seconds for permissions to be set up correctly for new cluster - sleep 10 - - # Open applications tab - show.open_applications - - # Helm must be installed before everything else - show.install!(:helm) - show.await_installed(:helm) - - show.install!(:ingress) if @install_ingress - show.install!(:prometheus) if @install_prometheus - show.install!(:runner) if @install_runner - - show.await_installed(:ingress) if @install_ingress - show.await_installed(:prometheus) if @install_prometheus - show.await_installed(:runner) if @install_runner - - if @install_ingress - populate(:ingress_ip) - - show.open_details - show.set_domain("#{ingress_ip}.nip.io") - show.save_domain - end - end - end - end - end - end -end diff --git a/qa/qa/resource/kubernetes_cluster/base.rb b/qa/qa/resource/kubernetes_cluster/base.rb new file mode 100644 index 00000000000..38bca48be17 --- /dev/null +++ b/qa/qa/resource/kubernetes_cluster/base.rb @@ -0,0 +1,40 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + module KubernetesCluster + class Base < Resource::Base + attr_writer :add_name_uuid + + attribute :id + attribute :name + attribute :domain + attribute :enabled + attribute :managed + attribute :management_project_id + + attribute :api_url + attribute :token + attribute :ca_cert + attribute :namespace + + attribute :authorization_type + attribute :environment_scope + + def initialize + @add_name_uuid = true + @enabled = true + @managed = true + @authorization_type = :rbac + @environment_scope = :* + end + + def name=(new_name) + @name = @add_name_uuid ? "#{new_name}-#{SecureRandom.hex(5)}" : new_name + end + end + end + end +end diff --git a/qa/qa/resource/kubernetes_cluster/project_cluster.rb b/qa/qa/resource/kubernetes_cluster/project_cluster.rb new file mode 100644 index 00000000000..5c61cc29236 --- /dev/null +++ b/qa/qa/resource/kubernetes_cluster/project_cluster.rb @@ -0,0 +1,72 @@ +# frozen_string_literal: true + +module QA + module Resource + module KubernetesCluster + class ProjectCluster < Base + attr_writer :cluster, + :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner, :domain + + attribute :project do + Resource::Project.fabricate! + end + + attribute :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform( + &:go_to_operations_kubernetes) + + Page::Project::Operations::Kubernetes::Index.perform( + &:add_kubernetes_cluster) + + Page::Project::Operations::Kubernetes::Add.perform( + &:add_existing_cluster) + + Page::Project::Operations::Kubernetes::AddExisting.perform do |cluster_page| + cluster_page.set_cluster_name(@cluster.cluster_name) + cluster_page.set_api_url(@cluster.api_url) + cluster_page.set_ca_certificate(@cluster.ca_certificate) + cluster_page.set_token(@cluster.token) + cluster_page.uncheck_rbac! unless @cluster.rbac + cluster_page.add_cluster! + end + + if @install_helm_tiller + Page::Project::Operations::Kubernetes::Show.perform do |show| + # We must wait a few seconds for permissions to be set up correctly for new cluster + sleep 10 + + # Open applications tab + show.open_applications + + # Helm must be installed before everything else + show.install!(:helm) + show.await_installed(:helm) + + show.install!(:ingress) if @install_ingress + show.install!(:prometheus) if @install_prometheus + show.install!(:runner) if @install_runner + + show.await_installed(:ingress) if @install_ingress + show.await_installed(:prometheus) if @install_prometheus + show.await_installed(:runner) if @install_runner + + if @install_ingress + populate(:ingress_ip) + + show.open_details + show.set_domain("#{ingress_ip}.nip.io") + show.save_domain + end + end + end + end + end + end + end +end diff --git a/qa/qa/resource/pipeline.rb b/qa/qa/resource/pipeline.rb new file mode 100644 index 00000000000..a115de3e825 --- /dev/null +++ b/qa/qa/resource/pipeline.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module QA + module Resource + class Pipeline < Base + attribute :project do + Resource::Project.fabricate! do |project| + project.name = 'project-with-pipeline' + end + end + + attribute :id + attribute :status + attribute :ref + attribute :sha + + # array in form + # [ + # { key: 'UPLOAD_TO_S3', variable_type: 'file', value: true }, + # { key: 'SOMETHING', variable_type: 'env_var', value: 'yes' } + # ] + attribute :variables + + def initialize + @ref = 'master' + @variables = [] + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:click_run_pipeline_button) + Page::Project::Pipeline::New.perform(&:click_run_pipeline_button) + end + + def api_get_path + "/projects/#{project.id}/pipelines/#{id}" + end + + def api_post_path + "/projects/#{project.id}/pipeline" + end + + def api_post_body + { + ref: ref, + variables: variables + } + end + end + end +end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index cb047f81d02..78e2ba8a248 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -7,11 +7,11 @@ module QA class Project < Base include Events::Project include Members + include Visibility attr_accessor :repository_storage # requires admin access attr_writer :initialize_with_readme attr_writer :auto_devops_enabled - attr_writer :visibility attribute :id attribute :name @@ -19,6 +19,8 @@ module QA attribute :description attribute :standalone attribute :runners_token + attribute :visibility + attribute :template_name attribute :group do Group.fabricate! @@ -50,7 +52,8 @@ module QA @description = 'My awesome project' @initialize_with_readme = false @auto_devops_enabled = false - @visibility = 'public' + @visibility = :public + @template_name = nil end def name=(raw_name) @@ -63,6 +66,13 @@ module QA Page::Group::Show.perform(&:go_to_new_project) end + if @template_name + Page::Project::New.perform do |new_page| + new_page.click_create_from_template_tab + new_page.use_template_for_project(@template_name) + end + end + Page::Project::New.perform do |new_page| new_page.choose_test_namespace new_page.choose_name(@name) @@ -83,6 +93,10 @@ module QA "/projects/#{CGI.escape(path_with_namespace)}" end + def api_visibility_path + "/projects/#{id}" + end + def api_get_archive_path(type = 'tar.gz') "#{api_get_path}/repository/archive.#{type}" end @@ -95,6 +109,10 @@ module QA "#{api_get_path}/runners" end + def api_pipelines_path + "#{api_get_path}/pipelines" + end + def api_put_path "/projects/#{id}" end @@ -118,6 +136,7 @@ module QA end post_body[:repository_storage] = repository_storage if repository_storage + post_body[:template_name] = @template_name if @template_name post_body end @@ -152,10 +171,19 @@ module QA end def runners(tag_list: nil) - response = get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url + response = if tag_list + get Runtime::API::Request.new(api_client, "#{api_runners_path}?tag_list=#{tag_list.compact.join(',')}").url + else + get Runtime::API::Request.new(api_client, "#{api_runners_path}").url + end + parse_body(response) end + def pipelines + parse_body(get(Runtime::API::Request.new(api_client, api_pipelines_path).url)) + end + def share_with_group(invitee, access_level = Resource::Members::AccessLevel::DEVELOPER) post Runtime::API::Request.new(api_client, "/projects/#{id}/share").url, { group_id: invitee.id, group_access: access_level } end diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb index f1f72c9cacd..b2a36f92ffe 100644 --- a/qa/qa/resource/runner.rb +++ b/qa/qa/resource/runner.rb @@ -5,8 +5,8 @@ require 'securerandom' module QA module Resource class Runner < Base - attr_writer :name, :tags, :image - attr_accessor :config, :token + attr_writer :name, :tags, :image, :executor, :executor_image + attr_accessor :config, :token, :run_untagged attribute :id attribute :project do @@ -20,35 +20,42 @@ module QA @name || "qa-runner-#{SecureRandom.hex(4)}" end - def tags - @tags || %w[qa e2e] - end - def image @image || 'gitlab/gitlab-runner:alpine' end + def executor + @executor || :shell + end + + def executor_image + @executor_image || 'registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6' + end + def fabricate_via_api! Service::DockerRun::GitlabRunner.new(name).tap do |runner| runner.pull runner.token = @token ||= project.runners_token runner.address = Runtime::Scenario.gitlab_address - runner.tags = tags + runner.tags = @tags if @tags runner.image = image runner.config = config if config + runner.executor = executor + runner.executor_image = executor_image if executor == :docker + runner.run_untagged = run_untagged if run_untagged runner.register! end end def remove_via_api! - runners = project.runners(tag_list: tags) + runners = project.runners(tag_list: @tags) unless runners && !runners.empty? - raise "Project #{project.path_with_namespace} has no runners with tags #{tags}." + raise "Project #{project.path_with_namespace} has no runners#{" with tags #{@tags}." if @tags&.any?}" end this_runner = runners.find { |runner| runner[:description] == name } unless this_runner - raise "Project #{project.path_with_namespace} does not have a runner with a description matching #{name} and tags #{tags}. Runners available: #{runners}" + raise "Project #{project.path_with_namespace} does not have a runner with a description matching #{name} #{"or tags #{@tags}" if @tags&.any?}. Runners available: #{runners}" end @id = this_runner[:id] diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index 3e130aef9e4..b948bf3969b 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -5,12 +5,16 @@ module QA class SSHKey < Base extend Forwardable - attr_accessor :title + attr_reader :title attribute :id def_delegators :key, :private_key, :public_key, :md5_fingerprint + def initialize + self.title = Time.now.to_f + end + def key @key ||= Runtime::Key::RSA.new end @@ -28,6 +32,10 @@ module QA api_post end + def title=(title) + @title = "E2E test key: #{title}" + end + def api_delete QA::Runtime::Logger.debug("Deleting SSH key with title '#{title}' and fingerprint '#{md5_fingerprint}'") diff --git a/qa/qa/resource/visibility.rb b/qa/qa/resource/visibility.rb new file mode 100644 index 00000000000..b31bd3fca49 --- /dev/null +++ b/qa/qa/resource/visibility.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module QA + module Resource + module Visibility + def set_visibility(visibility) + put Runtime::API::Request.new(api_client, api_visibility_path).url, { visibility: visibility } + end + + class VisibilityLevel + %i(public internal private).each do |level| + const_set(level.upcase, level) + end + end + end + end +end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index b9a3c9184aa..d29571df981 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'airborne' - module QA module Runtime module API diff --git a/qa/qa/runtime/project.rb b/qa/qa/runtime/project.rb deleted file mode 100644 index 89edfee1fbe..00000000000 --- a/qa/qa/runtime/project.rb +++ /dev/null @@ -1,35 +0,0 @@ -# frozen_string_literal: true - -module QA - module Runtime - module Project - extend self - extend Support::Api - - def create_project(project_name, api_client, project_description = 'default') - project = Resource::Project.fabricate_via_api! do |project| - project.add_name_uuid = false - project.name = project_name - project.description = project_description - project.api_client = api_client - project.visibility = 'public' - end - project - end - - def push_file_to_project(target_project, file_name, file_content) - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = target_project - push.file_name = file_name - push.file_content = file_content - end - end - - def set_project_visibility(api_client, project_id, visibility) - request = Runtime::API::Request.new(api_client, "/projects/#{project_id}") - response = put request.url, visibility: visibility - response.code.equal?(QA::Support::Api::HTTP_STATUS_OK) - end - end - end -end diff --git a/qa/qa/scenario/test/integration/gitaly_ha.rb b/qa/qa/scenario/test/integration/gitaly_ha.rb new file mode 100644 index 00000000000..dbca1a1dd6d --- /dev/null +++ b/qa/qa/scenario/test/integration/gitaly_ha.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class GitalyHA < Test::Instance::All + tags :gitaly_ha + end + end + end + end +end diff --git a/qa/qa/service/docker_run/gitlab_runner.rb b/qa/qa/service/docker_run/gitlab_runner.rb index 6856a5a8399..834f6b430ac 100644 --- a/qa/qa/service/docker_run/gitlab_runner.rb +++ b/qa/qa/service/docker_run/gitlab_runner.rb @@ -6,14 +6,21 @@ module QA module Service module DockerRun class GitlabRunner < Base - attr_accessor :token, :address, :tags, :image, :run_untagged - attr_writer :config + attr_reader :tags + attr_accessor :token, :address, :image, :run_untagged + attr_writer :config, :executor, :executor_image + + CONFLICTING_VARIABLES_MESSAGE = <<~MSG + There are conflicting options preventing the runner from starting. + %s cannot be specified if %s is %s + MSG def initialize(name) @image = 'gitlab/gitlab-runner:alpine' @name = name || "qa-runner-#{SecureRandom.hex(4)}" - @tags = %w[qa test] - @run_untagged = false + @run_untagged = true + @executor = :shell + @executor_image = 'registry.gitlab.com/gitlab-org/gitlab-build-images:gitlab-qa-alpine-ruby-2.6' super() end @@ -32,23 +39,49 @@ module QA shell <<~CMD.tr("\n", ' ') docker run -d --rm --entrypoint=/bin/sh --network #{network} --name #{@name} - -p 8093:8093 - -e CI_SERVER_URL=#{@address} - -e REGISTER_NON_INTERACTIVE=true - -e REGISTRATION_TOKEN=#{@token} - -e RUNNER_EXECUTOR=shell - -e RUNNER_TAG_LIST=#{@tags.join(',')} - -e RUNNER_NAME=#{@name} + #{'-v /var/run/docker.sock:/var/run/docker.sock' if @executor == :docker} + --privileged #{@image} -c "#{register_command}" CMD end + def tags=(tags) + @tags = tags + @run_untagged = false + end + private def register_command - <<~CMD + args = [] + args << '--non-interactive' + args << "--name #{@name}" + args << "--url #{@address}" + args << "--registration-token #{@token}" + + args << if run_untagged + raise CONFLICTING_VARIABLES_MESSAGE % [:tags=, :run_untagged, run_untagged] if @tags&.any? + + '--run-untagged=true' + else + raise 'You must specify tags to run!' unless @tags&.any? + + "--tag-list #{@tags.join(',')}" + end + + args << "--executor #{@executor}" + + if @executor == :docker + args << "--docker-image #{@executor_image}" + args << '--docker-tlsverify=false' + args << '--docker-privileged=true' + args << "--docker-network-mode=#{network}" + end + + <<~CMD.strip printf '#{config.chomp.gsub(/\n/, "\\n").gsub('"', '\"')}' > /etc/gitlab-runner/config.toml && - gitlab-runner register --run-untagged=#{@run_untagged} && + gitlab-runner register \ + #{args.join(' ')} && gitlab-runner run CMD end diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb new file mode 100644 index 00000000000..d8fa72456ad --- /dev/null +++ b/qa/qa/service/praefect_manager.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Service + class PraefectManager + include Service::Shellout + + def initialize + @praefect = 'praefect' + @first_node = 'gitaly1' + @second_node = 'gitaly2' + @primary_node = @first_node + @secondary_node = @second_node + end + + def stop_primary_node + shell "docker stop #{@primary_node}" + @secondary_node, @primary_node = @primary_node, @secondary_node + end + + def reset + shell "docker start #{@primary_node}" + shell "docker start #{@secondary_node}" + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb index 819739ac535..1bf435014af 100644 --- a/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb +++ b/qa/qa/specs/features/api/1_manage/rate_limits_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'airborne' + module QA context 'Manage with IP rate limits', :requires_admin do describe 'Users API' do diff --git a/qa/qa/specs/features/api/1_manage/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb index ba1ba204d24..fbc26e81b69 100644 --- a/qa/qa/specs/features/api/1_manage/users_spec.rb +++ b/qa/qa/specs/features/api/1_manage/users_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'airborne' + module QA context 'Manage' do describe 'Users API' do diff --git a/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb b/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb index f14fcc5afce..58d716f759e 100644 --- a/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb +++ b/qa/qa/specs/features/api/2_plan/closes_issue_via_pushing_a_commit_spec.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'airborne' + module QA context 'Plan' do include Support::Api diff --git a/qa/qa/specs/features/api/3_create/repository/files_spec.rb b/qa/qa/specs/features/api/3_create/repository/files_spec.rb index dc471128dae..92858ba4107 100644 --- a/qa/qa/specs/features/api/3_create/repository/files_spec.rb +++ b/qa/qa/specs/features/api/3_create/repository/files_spec.rb @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'airborne' require 'securerandom' module QA 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 5ba434a7781..3ad56e21ad4 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 @@ -1,5 +1,6 @@ # frozen_string_literal: true +require 'airborne' require 'securerandom' require 'digest' diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb index 5f7a6981f23..0a577aa07f8 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb @@ -3,6 +3,8 @@ module QA context 'Plan', :orchestrated, :smtp do describe 'Email Notification' do + include Support::Api + let(:user) do Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) 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 2cb6a76b6b3..7b4418191a3 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 @@ -17,7 +17,7 @@ module QA end end - context 'when using attachments in comments', :object_storage, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/issues/205408', type: :bug } do + 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)) diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb new file mode 100644 index 00000000000..409c7d321f0 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/issue_boards/focus_mode_spec.rb @@ -0,0 +1,28 @@ +# frozen_string_literal: true + +module QA + context 'Plan', :reliable do + describe 'Issue board focus mode' do + let(:project) do + QA::Resource::Project.fabricate_via_api! do |project| + project.name = 'sample-project-issue-board-focus-mode' + end + end + + before do + Flow::Login.sign_in + end + + it 'focuses on issue board' do + project.visit! + + Page::Project::Menu.perform(&:go_to_boards) + EE::Page::Component::IssueBoard::Show.perform do |show| + show.click_focus_mode_button + + expect(show.focused_board).to be_visible + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb new file mode 100644 index 00000000000..b50edcfcf08 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_add_annotation.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe '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(:annotation) { "This design is great!" } + + before do + Flow::Login.sign_in + end + + it 'user adds a design and annotation' do + issue.visit! + + Page::Project::Issue::Show.perform do |show| + show.click_designs_tab + show.add_design(design) + show.click_design(design_filename) + show.add_annotation(annotation) + + expect(show).to have_annotation(annotation) + + show.click_discussion_tab + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb b/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb new file mode 100644 index 00000000000..3bb03f68d51 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/gitaly/high_availability_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + context 'Gitaly' do + describe 'High Availability', :orchestrated, :gitaly_ha do + let(:project) do + Resource::Project.fabricate! do |project| + project.name = 'gitaly_high_availability' + end + end + let(:initial_file) { 'pushed_to_primary.txt' } + let(:final_file) { 'pushed_to_secondary.txt' } + + before do + @praefect_manager = Service::PraefectManager.new + Flow::Login.sign_in + end + + after do + @praefect_manager.reset + end + + it 'makes sure that automatic failover is happening' do + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.commit_message = 'pushed to primary gitaly node' + push.new_branch = true + push.file_name = initial_file + push.file_content = "This should exist on both nodes" + end + + @praefect_manager.stop_primary_node + + project.visit! + + Page::Project::Show.perform do |show| + show.wait_until do + show.has_name?(project.name) + end + expect(show).to have_file(initial_file) + end + + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.add_files([ + { + file_path: 'committed_to_primary.txt', + content: 'This should exist on both nodes too' + } + ]) + end + + project.visit! + + Page::Project::Show.perform do |show| + expect(show).to have_file(final_file) + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 7d4e6b7efbc..3964ae7eada 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -10,6 +10,8 @@ module QA merge_request.fork_branch = 'feature-branch' end + merge_request.project.api_put(auto_devops_enabled: false) + Page::Main::Menu.perform(&:sign_out) Page::Main::Login.perform(&:sign_in_using_credentials) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index 25866e12185..68bbc1719fc 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -12,7 +12,7 @@ module QA resource.title = key_title end - expect(page).to have_content("Title: #{key_title}") + expect(page).to have_content(key.title) expect(page).to have_content(key.md5_fingerprint) Page::Main::Menu.perform(&:click_settings_link) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb index 13fe8918f97..d0123da53bb 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/move_project_create_fork_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - context 'Create' do - describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin, quarantine: { type: :new } do + context 'Create', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217002', type: :investigating } do + describe 'Gitaly repository storage', :orchestrated, :repository_storage, :requires_admin do let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } let(:parent_project) do Resource::Project.fabricate_via_api! do |project| 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 9bc4dcbca2a..9b504ad76b4 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 @@ -3,6 +3,8 @@ module QA context 'Create', :requires_admin do describe 'push after setting the file size limit via admin/application_settings' do + include Support::Api + before(:context) do @project = Resource::Project.fabricate_via_api! do |p| p.name = 'project-test-push-limit' @@ -39,12 +41,10 @@ module QA def set_file_size_limit(limit) request = Runtime::API::Request.new(@api_client, '/application/settings') - put request.url, receive_max_input_size: limit + response = put request.url, receive_max_input_size: limit - expect_status(200) - expect(json_body).to match( - a_hash_including(receive_max_input_size: limit) - ) + expect(response.code).to eq(200) + expect(parse_body(response)[:receive_max_input_size]).to eq(limit) end def push_new_file(file_name, wait_for_push: true) 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 new file mode 100644 index 00000000000..341ff39fdf1 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Version control for personal snippets' do + let(:new_file) { 'new_snippet_file' } + let(:changed_content) { 'changes' } + let(:commit_message) { 'Changes to snippets' } + let(:added_content) { 'updated ' } + let(:branch_name) { 'master' } + + let(:snippet) do + Resource::Snippet.fabricate! do |snippet| + snippet.file_name = new_file + end + end + + let(:ssh_key) do + Resource::SSHKey.fabricate_via_api! do |resource| + resource.title = "my key title #{Time.now.to_f}" + end + end + + let(:repository_uri_http) do + snippet + Page::Dashboard::Snippet::Show.perform(&:get_repository_uri_http) + end + + let(:repository_uri_ssh) do + ssh_key + snippet + Page::Dashboard::Snippet::Show.perform(&:get_repository_uri_ssh) + end + + before do + Flow::Login.sign_in + end + + it 'clones, pushes, and pulls a snippet over HTTP, edits via UI' do + Resource::Repository::Push.fabricate! do |push| + push.repository_http_uri = repository_uri_http + push.file_name = new_file + push.file_content = changed_content + push.commit_message = commit_message + push.new_branch = false + end + + page.refresh + verify_changes_in_ui + + Page::Dashboard::Snippet::Show.perform(&:click_edit_button) + + Page::Dashboard::Snippet::Edit.perform do |snippet| + snippet.add_to_file_content(added_content) + snippet.save_changes + end + + Git::Repository.perform do |repository| + repository.init_repository + repository.pull(repository_uri_http, branch_name) + + expect(repository.commits.size).to eq(3) + expect(repository.commits.first).to include('Update snippet') + expect(repository.file_content(new_file)).to include("#{added_content}#{changed_content}") + end + end + + it 'clones, pushes, and pulls a snippet over SSH, deletes via UI' do + Resource::Repository::Push.fabricate! do |push| + push.repository_ssh_uri = repository_uri_ssh + push.ssh_key = ssh_key + push.file_name = new_file + push.file_content = changed_content + push.commit_message = commit_message + push.new_branch = false + end + + page.refresh + verify_changes_in_ui + + Page::Dashboard::Snippet::Show.perform(&:click_delete_button) + + # attempt to pull a deleted snippet, get a missing repository error + Git::Repository.perform do |repository| + repository.uri = repository_uri_ssh + repository.use_ssh_key(ssh_key) + 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.]+/) + end + end + + def verify_changes_in_ui + Page::Dashboard::Snippet::Show.perform do |snippet| + expect(snippet).to have_file_name(new_file) + expect(snippet).to have_file_content(changed_content) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb index d38b8560a38..dfcbf4b44c8 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_snippet_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Create', :smoke, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/215031', type: :investigating } do + context 'Create', :smoke do describe 'Snippet creation' do it 'User creates a snippet' do Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb index 8ea1534492c..ed988bdf046 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/review_merge_request_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context 'Create', quarantine: { type: :new } do + context 'Create' do describe 'Review a merge request in Web IDE' do let(:new_file) { 'awesome_new_file.txt' } let(:original_text) { 'Text' } 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 2550ee7193b..f8a589ad93b 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 @@ -7,6 +7,7 @@ module QA let!(:runner) do Resource::Runner.fabricate! do |runner| runner.name = executor + runner.tags = ['e2e-test'] end end 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 new file mode 100644 index 00000000000..4d549dde858 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/testing/view_code_coverage_spec.rb @@ -0,0 +1,61 @@ +# frozen_string_literal: true + +module QA + context 'Verify', :docker, :runner do + describe 'Code coverage statistics' do + let(:simplecov) { '\(\d+.\d+\%\) covered' } + let(:executor) { "qa-runner-#{Time.now.to_i}" } + let(:runner) do + Resource::Runner.fabricate_via_api! do |runner| + runner.name = executor + runner.tags = ['e2e-test'] + end + end + + let(:merge_request) do + Resource::MergeRequest.fabricate_via_api! do |mr| + mr.project = runner.project + mr.file_name = '.gitlab-ci.yml' + mr.file_content = <<~EOF + test: + tags: [e2e-test] + script: + - echo '(66.67%) covered' + EOF + end + end + + before do + Flow::Login.sign_in + end + + after do + runner.remove_via_api! + end + + it 'creates an MR with code coverage statistics' do + runner.project.visit! + configure_code_coverage(simplecov) + merge_request.visit! + + Page::MergeRequest::Show.perform do |mr_widget| + Support::Retrier.retry_until(max_attempts: 5, sleep_interval: 5) do + mr_widget.has_pipeline_status?(/Pipeline #\d+ passed/) + end + expect(mr_widget).to have_content('Coverage 66.67%') + end + end + end + + private + + def configure_code_coverage(coverage_tool_pattern) + Page::Project::Menu.perform(&:go_to_ci_cd_settings) + Page::Project::Settings::CICD.perform do |settings| + settings.expand_general_pipelines do |coverage| + coverage.configure_coverage_regex(coverage_tool_pattern) + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb b/qa/qa/specs/features/browser_ui/6_release/pipeline/parent_child_pipelines_dependent_relationship_spec.rb index e71212bcb68..b1eb26f0d63 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 - context 'Release', :docker, quarantine: { type: :new } do + context 'Release', :docker, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217250', type: :investigating } 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 633af9c2e8a..c9a61fc6305 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 - context 'Release', :docker, quarantine: { type: :new } do + context 'Release', :docker, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217250', type: :investigating } 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 0a52b01af03..292fc40bec4 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 @@ -35,7 +35,7 @@ module QA end # Connect K8s cluster - Resource::KubernetesCluster.fabricate! do |k8s_cluster| + Resource::KubernetesCluster::ProjectCluster.fabricate! do |k8s_cluster| k8s_cluster.project = project k8s_cluster.cluster = cluster k8s_cluster.install_helm_tiller = true diff --git a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb index 9a52109c8cb..04c68598239 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/kubernetes/kubernetes_integration_spec.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - context 'Configure' do - describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin, quarantine: { type: :new } do + context 'Configure', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/209085', type: :investigating } do + describe 'Kubernetes Cluster Integration', :orchestrated, :kubernetes, :requires_admin do context 'Project Clusters' do let(:cluster) { Service::KubernetesCluster.new(provider_class: Service::ClusterProvider::K3s).create! } let(:project) do @@ -21,12 +21,10 @@ module QA end it 'can create and associate a project cluster', :smoke do - Resource::KubernetesCluster.fabricate_via_browser_ui! do |k8s_cluster| + Resource::KubernetesCluster::ProjectCluster.fabricate_via_browser_ui! do |k8s_cluster| k8s_cluster.project = project k8s_cluster.cluster = cluster - end - - project.visit! + end.project.visit! Page::Project::Menu.perform(&:go_to_operations_kubernetes) diff --git a/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb new file mode 100644 index 00000000000..45273655bb6 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/8_monitor/all_monitor_core_features_spec.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +module QA + context 'Monitor', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217705', type: :flaky } do + describe 'with Prometheus Gitlab-managed cluster', :orchestrated, :kubernetes, :docker, :runner do + before :all do + Flow::Login.sign_in + @project, @runner = deploy_project_with_prometheus + end + + before do + Flow::Login.sign_in_unless_signed_in + @project.visit! + end + + after :all do + @runner.remove_via_api! + @cluster.remove! + end + + it 'configures custom metrics' do + verify_add_custom_metric + verify_edit_custom_metric + verify_delete_custom_metric + end + + it 'duplicates to create dashboard to custom' do + Page::Project::Menu.perform(&:go_to_operations_metrics) + + Page::Project::Operations::Metrics::Show.perform do |dashboard| + dashboard.duplicate_dashboard + + expect(dashboard).to have_metrics + expect(dashboard).to have_edit_dashboard_enabled + end + end + + it 'verifies data on filtered deployed environment' do + Page::Project::Menu.perform(&:go_to_operations_metrics) + + Page::Project::Operations::Metrics::Show.perform do |dashboard| + dashboard.filter_environment + + expect(dashboard).to have_metrics + end + end + + it 'filters using the quick range' do + Page::Project::Menu.perform(&:go_to_operations_metrics) + + Page::Project::Operations::Metrics::Show.perform do |dashboard| + dashboard.show_last('30 minutes') + expect(dashboard).to have_metrics + + dashboard.show_last('3 hours') + expect(dashboard).to have_metrics + + dashboard.show_last('1 day') + expect(dashboard).to have_metrics + end + end + + private + + def deploy_project_with_prometheus + project = Resource::Project.fabricate_via_api! do |project| + project.name = 'cluster-with-prometheus' + project.description = 'Cluster with Prometheus' + end + + runner = Resource::Runner.fabricate_via_api! do |runner| + runner.project = project + runner.name = project.name + end + + @cluster = Service::KubernetesCluster.new.create! + + cluster_props = Resource::KubernetesCluster::ProjectCluster.fabricate! do |cluster_settings| + cluster_settings.project = project + cluster_settings.cluster = @cluster + cluster_settings.install_helm_tiller = true + cluster_settings.install_ingress = true + cluster_settings.install_prometheus = true + end + + Resource::CiVariable.fabricate_via_api! do |ci_variable| + ci_variable.project = project + ci_variable.key = 'AUTO_DEVOPS_DOMAIN' + ci_variable.value = cluster_props.ingress_ip + ci_variable.masked = false + end + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.directory = Pathname + .new(__dir__) + .join('../../../../fixtures/monitored_auto_devops') + push.commit_message = 'Create AutoDevOps compatible Project for Monitoring' + end + + Page::Project::Menu.perform(&:click_ci_cd_pipelines) + Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success_or_retry) + + [project, runner] + end + + def verify_add_custom_metric + Page::Project::Menu.perform(&:go_to_integrations_settings) + Page::Project::Settings::Integrations.perform(&:click_on_prometheus_integration) + + Page::Project::Settings::Services::Prometheus.perform do |metrics_panel| + metrics_panel.click_on_new_metric + metrics_panel.add_custom_metric + end + + Page::Project::Menu.perform(&:go_to_operations_metrics) + + Page::Project::Operations::Metrics::Show.perform do |dashboard| + expect(dashboard).to have_custom_metric('HTTP Requests Total') + end + end + + def verify_edit_custom_metric + Page::Project::Menu.perform(&:go_to_integrations_settings) + Page::Project::Settings::Integrations.perform(&:click_on_prometheus_integration) + Page::Project::Settings::Services::Prometheus.perform do |metrics_panel| + metrics_panel.click_on_custom_metric('Business / HTTP Requests Total (req/sec)') + metrics_panel.edit_custom_metric + end + + Page::Project::Menu.perform(&:go_to_operations_metrics) + + Page::Project::Operations::Metrics::Show.perform do |dashboard| + expect(dashboard).to have_custom_metric('Throughput') + end + end + + def verify_delete_custom_metric + Page::Project::Menu.perform(&:go_to_integrations_settings) + Page::Project::Settings::Integrations.perform(&:click_on_prometheus_integration) + + Page::Project::Settings::Services::Prometheus.perform do |metrics_panel| + metrics_panel.click_on_custom_metric('Business / Throughput (req/sec)') + metrics_panel.delete_custom_metric + end + + Page::Project::Menu.perform(&:go_to_operations_metrics) + + Page::Project::Operations::Metrics::Show.perform do |dashboard| + expect(dashboard).not_to have_custom_metric('Throughput') + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb b/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb deleted file mode 100644 index f7463c69db1..00000000000 --- a/qa/qa/specs/features/browser_ui/8_monitor/apm/dashboards_spec.rb +++ /dev/null @@ -1,97 +0,0 @@ -# frozen_string_literal: true - -module QA - context 'Monitor' do - describe 'Dashboards', :orchestrated, :kubernetes, quarantine: { type: :new } do - before(:all) do - @cluster = Service::KubernetesCluster.new.create! - Flow::Login.sign_in - create_project_to_monitor - wait_for_deployment - end - - before do - Flow::Login.sign_in_unless_signed_in - @project.visit! - end - - after(:all) do - @cluster&.remove! - end - - it 'duplicates to create dashboard to custom' do - Page::Project::Menu.perform(&:go_to_operations_metrics) - - Page::Project::Operations::Metrics::Show.perform do |dashboard| - dashboard.duplicate_dashboard - - expect(dashboard).to have_metrics - expect(dashboard).to have_edit_dashboard_enabled - end - end - - it 'verifies data on filtered deployed environment' do - Page::Project::Menu.perform(&:go_to_operations_metrics) - - Page::Project::Operations::Metrics::Show.perform do |dashboard| - dashboard.filter_environment - - expect(dashboard).to have_metrics - end - end - - it 'filters using the quick range' do - Page::Project::Menu.perform(&:go_to_operations_metrics) - - Page::Project::Operations::Metrics::Show.perform do |dashboard| - dashboard.show_last('30 minutes') - expect(dashboard).to have_metrics - - dashboard.show_last('3 hours') - expect(dashboard).to have_metrics - - dashboard.show_last('1 day') - expect(dashboard).to have_metrics - end - end - - private - - def wait_for_deployment - Page::Project::Menu.perform(&:click_ci_cd_pipelines) - Page::Project::Pipeline::Index.perform(&:wait_for_latest_pipeline_success_or_retry) - Page::Project::Menu.perform(&:go_to_operations_metrics) - end - - def create_project_to_monitor - @project = Resource::Project.fabricate_via_api! do |project| - project.name = 'cluster-with-prometheus' - project.description = 'Cluster with Prometheus' - end - - @cluster_props = Resource::KubernetesCluster.fabricate_via_browser_ui! do |cluster_settings| - cluster_settings.project = @project - cluster_settings.cluster = @cluster - cluster_settings.install_helm_tiller = true - cluster_settings.install_ingress = true - cluster_settings.install_prometheus = true - end - - Resource::CiVariable.fabricate_via_api! do |ci_variable| - ci_variable.project = @project - ci_variable.key = 'AUTO_DEVOPS_DOMAIN' - ci_variable.value = @cluster_props.ingress_ip - ci_variable.masked = false - end - - Resource::Repository::ProjectPush.fabricate! do |push| - push.project = @project - push.directory = Pathname - .new(__dir__) - .join('../../../../../fixtures/monitored_auto_devops') - push.commit_message = 'Create AutoDevOps compatible Project for Monitoring' - end - end - end - end -end diff --git a/qa/qa/specs/features/sanity/framework_spec.rb b/qa/qa/specs/features/sanity/framework_spec.rb index aae0f0ade71..611c6c7b1ff 100644 --- a/qa/qa/specs/features/sanity/framework_spec.rb +++ b/qa/qa/specs/features/sanity/framework_spec.rb @@ -6,7 +6,7 @@ module QA it 'succeeds' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - expect(page).to have_text('Open source software to collaborate on code') + expect(page).to have_text('A complete DevOps platform') end end diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb index 8b14184f3b7..dd3a50ac128 100644 --- a/qa/qa/specs/helpers/quarantine.rb +++ b/qa/qa/specs/helpers/quarantine.rb @@ -2,83 +2,87 @@ require 'rspec/core' -module QA::Specs::Helpers - module Quarantine - include RSpec::Core::Pending +module QA + module Specs + module Helpers + module Quarantine + include RSpec::Core::Pending - extend self + extend self - def configure_rspec - RSpec.configure do |config| - config.before(:context, :quarantine) do - Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class) - end + def configure_rspec + RSpec.configure do |config| + config.before(:context, :quarantine) do + Quarantine.skip_or_run_quarantined_contexts(config.inclusion_filter.rules, self.class) + end - config.before do |example| - Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example) + config.before do |example| + Quarantine.skip_or_run_quarantined_tests_or_contexts(config.inclusion_filter.rules, example) + end + end end - end - end - # Skip the entire context if a context is quarantined. This avoids running - # before blocks unnecessarily. - def skip_or_run_quarantined_contexts(filters, example) - return unless example.metadata.key?(:quarantine) + # Skip the entire context if a context is quarantined. This avoids running + # before blocks unnecessarily. + def skip_or_run_quarantined_contexts(filters, example) + return unless example.metadata.key?(:quarantine) - skip_or_run_quarantined_tests_or_contexts(filters, example) - end + skip_or_run_quarantined_tests_or_contexts(filters, example) + end - # Skip tests in quarantine unless we explicitly focus on them. - def skip_or_run_quarantined_tests_or_contexts(filters, example) - if filters.key?(:quarantine) - included_filters = filters_other_than_quarantine(filters) + # Skip tests in quarantine unless we explicitly focus on them. + def skip_or_run_quarantined_tests_or_contexts(filters, example) + if filters.key?(:quarantine) + included_filters = filters_other_than_quarantine(filters) - # If :quarantine is focused, skip the test/context unless its metadata - # includes quarantine and any other filters - # E.g., Suppose a test is tagged :smoke and :quarantine, and another is tagged - # :ldap and :quarantine. If we wanted to run just quarantined smoke tests - # using `--tag quarantine --tag smoke`, without this check we'd end up - # running that ldap test as well because of the :quarantine metadata. - # We could use an exclusion filter, but this way the test report will list - # the quarantined tests when they're not run so that we're aware of them - skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters) - else - if example.metadata.key?(:quarantine) - quarantine_message = %w(In quarantine) - quarantine_tag = example.metadata[:quarantine] + # If :quarantine is focused, skip the test/context unless its metadata + # includes quarantine and any other filters + # E.g., Suppose a test is tagged :smoke and :quarantine, and another is tagged + # :ldap and :quarantine. If we wanted to run just quarantined smoke tests + # using `--tag quarantine --tag smoke`, without this check we'd end up + # running that ldap test as well because of the :quarantine metadata. + # We could use an exclusion filter, but this way the test report will list + # the quarantined tests when they're not run so that we're aware of them + skip("Only running tests tagged with :quarantine and any of #{included_filters.keys}") if should_skip_when_focused?(example.metadata, included_filters) + else + if example.metadata.key?(:quarantine) + quarantine_message = %w(In quarantine) + quarantine_tag = example.metadata[:quarantine] - if !!quarantine_tag - quarantine_message << case quarantine_tag - when String - ": #{quarantine_tag}" - when Hash - ": #{quarantine_tag[:issue]}" - else - '' - end - end + if !!quarantine_tag + quarantine_message << case quarantine_tag + when String + ": #{quarantine_tag}" + when Hash + ": #{quarantine_tag[:issue]}" + else + '' + end + end - skip(quarantine_message.join(' ').strip) + skip(quarantine_message.join(' ').strip) + end + end end - end - end - def filters_other_than_quarantine(filter) - filter.reject { |key, _| key == :quarantine } - end + def filters_other_than_quarantine(filter) + filter.reject { |key, _| key == :quarantine } + end - # Checks if a test or context should be skipped. - # - # Returns true if - # - the metadata does not includes the :quarantine tag - # or if - # - the metadata includes the :quarantine tag - # - and the filter includes other tags that aren't in the metadata - def should_skip_when_focused?(metadata, included_filters) - return true unless metadata.key?(:quarantine) - return false if included_filters.empty? + # Checks if a test or context should be skipped. + # + # Returns true if + # - the metadata does not includes the :quarantine tag + # or if + # - the metadata includes the :quarantine tag + # - and the filter includes other tags that aren't in the metadata + def should_skip_when_focused?(metadata, included_filters) + return true unless metadata.key?(:quarantine) + return false if included_filters.empty? - (metadata.keys & included_filters.keys).empty? + (metadata.keys & included_filters.keys).empty? + end + end end end end diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index ac73cc00dbf..afeddeaa5d5 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -42,6 +42,8 @@ module QA tags_for_rspec.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled? + tags_for_rspec.push(%w[--tag ~skip_live_env]) if QA::Runtime::Env.dot_com? + QA::Runtime::Env.supported_features.each_key do |key| tags_for_rspec.push(%W[--tag ~requires_#{key}]) unless QA::Runtime::Env.can_test? key end diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 90924ffd40e..f5e4d4e294b 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'rest-client' + module QA module Support module Api @@ -7,6 +9,7 @@ module QA HTTP_STATUS_CREATED = 201 HTTP_STATUS_NO_CONTENT = 204 HTTP_STATUS_ACCEPTED = 202 + HTTP_STATUS_SERVER_ERROR = 500 def post(url, payload) RestClient::Request.execute( diff --git a/qa/qa/tools/delete_test_ssh_keys.rb b/qa/qa/tools/delete_test_ssh_keys.rb new file mode 100644 index 00000000000..953e9fc63d1 --- /dev/null +++ b/qa/qa/tools/delete_test_ssh_keys.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require_relative '../../qa' + +# This script deletes all selected test ssh keys for a specific user +# Keys can be selected by a string matching part of the key's title and by created date +# - Specify `title_portion` to delete only keys that include the string provided +# - Specify `delete_before` to delete only keys that were created before the given date +# +# If `dry_run` is true the script will list the keys by title and indicate whether each will be deleted +# +# Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS +# - GITLAB_QA_ACCESS_TOKEN should have API access and belong to the user whose keys will be deleted + +module QA + module Tools + class DeleteTestSSHKeys + include Support::Api + + ITEMS_PER_PAGE = '100' + + def initialize(title_portion: 'E2E test key:', delete_before: Date.today.to_s, dry_run: false) + raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] + raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN'] + + @api_client = Runtime::API::Client.new(ENV['GITLAB_ADDRESS'], personal_access_token: ENV['GITLAB_QA_ACCESS_TOKEN']) + @title_portion = title_portion + @delete_before = Date.parse(delete_before) + @dry_run = dry_run + end + + def run + STDOUT.puts 'Running...' + + keys_head_response = head Runtime::API::Request.new(@api_client, "/user/keys", per_page: ITEMS_PER_PAGE).url + total_pages = keys_head_response.headers[:x_total_pages] + + test_ssh_key_ids = fetch_test_ssh_key_ids(total_pages) + STDOUT.puts "Number of test ssh keys to be deleted: #{test_ssh_key_ids.length}" + + return if dry_run? + + delete_ssh_keys(test_ssh_key_ids) unless test_ssh_key_ids.empty? + STDOUT.puts "\nDone" + end + + private + + attr_reader :dry_run + alias_method :dry_run?, :dry_run + + def delete_ssh_keys(ssh_key_ids) + STDOUT.puts "Deleting #{ssh_key_ids.length} ssh keys..." + ssh_key_ids.each do |key_id| + delete_response = delete Runtime::API::Request.new(@api_client, "/user/keys/#{key_id}").url + dot_or_f = delete_response.code == 204 ? "\e[32m.\e[0m" : "\e[31mF\e[0m" + print dot_or_f + end + end + + def fetch_test_ssh_key_ids(pages) + key_ids = [] + + pages.to_i.times do |page_no| + get_keys_response = get Runtime::API::Request.new(@api_client, "/user/keys", page: (page_no + 1).to_s, per_page: ITEMS_PER_PAGE).url + keys = JSON.parse(get_keys_response.body).select do |key| + to_delete = key['title'].include?(@title_portion) && Date.parse(key['created_at']) < @delete_before + + puts "Key title: #{key['title']}\tcreated_at: #{key['created_at']}\tdelete? #{to_delete}" if dry_run? + + to_delete + end + key_ids.concat(keys.map { |key| key['id'] }) + end + + key_ids.uniq + end + end + end +end diff --git a/qa/qa/vendor/jenkins/page/configure_job.rb b/qa/qa/vendor/jenkins/page/configure_job.rb index 56a2602a003..471567ec828 100644 --- a/qa/qa/vendor/jenkins/page/configure_job.rb +++ b/qa/qa/vendor/jenkins/page/configure_job.rb @@ -9,16 +9,19 @@ module QA class ConfigureJob < Page::Base attr_accessor :job_name - def initialize - @path = "/job/#{@job_name}/configure" + def path + "/job/#{@job_name}/configure" end def configure(scm_url:) set_git_source_code_management_url(scm_url) click_build_when_change_is_pushed_to_gitlab set_publish_status_to_gitlab - click_save - wait_for_configuration_to_save + + Support::Retrier.retry_until(sleep_interval: 0.5) do + click_save + wait_for_configuration_to_save + end end private @@ -58,8 +61,8 @@ module QA end def wait_for_configuration_to_save - QA::Support::Waiter.wait_until(sleep_interval: 1.0) do - !page.current_url.include?(@path) + QA::Support::Waiter.wait_until(max_duration: 10, raise_on_failure: false) do + !page.current_url.include?(path) end end end diff --git a/qa/qa/vendor/jenkins/page/last_job_console.rb b/qa/qa/vendor/jenkins/page/last_job_console.rb index f41b91c2cdb..9fcbb8ab956 100644 --- a/qa/qa/vendor/jenkins/page/last_job_console.rb +++ b/qa/qa/vendor/jenkins/page/last_job_console.rb @@ -9,6 +9,8 @@ module QA class LastJobConsole < Page::Base attr_accessor :job_name + CONSOLE_OUTPUT_SELECTOR = '.console-output' + def path "/job/#{@job_name}/lastBuild/console" end @@ -17,13 +19,23 @@ module QA # Retry on errors such as: # Selenium::WebDriver::Error::JavascriptError: # javascript error: this.each is not a function - Support::Retrier.retry_on_exception(reload_page: page) do - page.has_text?('Finished: SUCCESS') + Support::Retrier.retry_on_exception(reload_page: page, sleep_interval: 1) do + has_console_output? && console_output.include?('Finished: SUCCESS') end end def no_failed_status_update? - page.has_no_text?('Failed to update Gitlab commit status') + !console_output.include?('Failed to update Gitlab commit status') + end + + private + + def has_console_output? + page.has_selector?(CONSOLE_OUTPUT_SELECTOR, wait: 1) + end + + def console_output + page.find(CONSOLE_OUTPUT_SELECTOR).text end end end |