diff options
author | Filipe Freire <livrofubia@gmail.com> | 2018-01-25 20:23:19 +0000 |
---|---|---|
committer | Filipe Freire <livrofubia@gmail.com> | 2018-01-25 20:23:19 +0000 |
commit | 03b87ec943ec9e48106a8d94350f6d5011e30519 (patch) | |
tree | 77bc2b00229c4d5598cb94db379f6beae240f792 /qa | |
parent | 350dbca41c2be6717d4c9f5800ef6dd60b06d932 (diff) | |
parent | 10d8026f69efe83a0f4759f91c5087effd676191 (diff) | |
download | gitlab-ce-03b87ec943ec9e48106a8d94350f6d5011e30519.tar.gz |
Merge branch 'master' of https://gitlab.com/filipefreire/gitlab-ce into filipefreire_155
Diffstat (limited to 'qa')
47 files changed, 963 insertions, 164 deletions
diff --git a/qa/Gemfile b/qa/Gemfile index 4c866a3f893..c3e61568f3d 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -6,3 +6,5 @@ gem 'capybara-screenshot', '~> 1.0.18' gem 'rake', '~> 12.3.0' gem 'rspec', '~> 3.7' gem 'selenium-webdriver', '~> 3.8.0' +gem 'net-ssh', require: false +gem 'airborne', '~> 0.2.13' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 88d5fe834a0..51d2e4d7a10 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -1,8 +1,19 @@ GEM remote: https://rubygems.org/ specs: + activesupport (5.1.4) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (~> 0.7) + minitest (~> 5.1) + tzinfo (~> 1.1) addressable (2.5.2) public_suffix (>= 2.0.2, < 4.0) + airborne (0.2.13) + activesupport + rack + rack-test (~> 0.6, >= 0.6.2) + rest-client (>= 1.7.3, < 3.0) + rspec (~> 3.1) byebug (9.1.0) capybara (2.16.1) addressable @@ -17,13 +28,26 @@ GEM childprocess (0.8.0) ffi (~> 1.0, >= 1.0.11) coderay (1.1.2) + concurrent-ruby (1.0.5) diff-lcs (1.3) + domain_name (0.5.20170404) + unf (>= 0.0.5, < 1.0.0) ffi (1.9.18) + http-cookie (1.0.3) + domain_name (~> 0.5) + i18n (0.9.1) + concurrent-ruby (~> 1.0) launchy (2.4.3) addressable (~> 2.3) method_source (0.9.0) + mime-types (3.1) + mime-types-data (~> 3.2015) + mime-types-data (3.2016.0521) mini_mime (1.0.0) mini_portile2 (2.3.0) + minitest (5.11.1) + net-ssh (4.1.0) + netrc (0.11.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) pry (0.11.3) @@ -37,11 +61,15 @@ GEM rack-test (0.8.2) rack (>= 1.0, < 3) rake (12.3.0) + rest-client (2.0.2) + http-cookie (>= 1.0.2, < 2.0) + mime-types (>= 1.16, < 4.0) + netrc (~> 0.8) rspec (3.7.0) rspec-core (~> 3.7.0) rspec-expectations (~> 3.7.0) rspec-mocks (~> 3.7.0) - rspec-core (3.7.0) + rspec-core (3.7.1) rspec-support (~> 3.7.0) rspec-expectations (3.7.0) diff-lcs (>= 1.2.0, < 2.0) @@ -54,6 +82,12 @@ GEM selenium-webdriver (3.8.0) childprocess (~> 0.5) rubyzip (~> 1.0) + thread_safe (0.3.6) + tzinfo (1.2.4) + thread_safe (~> 0.1) + unf (0.1.4) + unf_ext + unf_ext (0.0.7.4) xpath (2.1.0) nokogiri (~> 1.3) @@ -61,12 +95,14 @@ PLATFORMS ruby DEPENDENCIES + airborne (~> 0.2.13) capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) + net-ssh pry-byebug (~> 3.5.1) rake (~> 12.3.0) rspec (~> 3.7) selenium-webdriver (~> 3.8.0) BUNDLED WITH - 1.16.0 + 1.16.1 @@ -11,6 +11,9 @@ module QA autoload :Scenario, 'qa/runtime/scenario' autoload :Browser, 'qa/runtime/browser' autoload :Env, 'qa/runtime/env' + autoload :RSAKey, 'qa/runtime/rsa_key' + autoload :Address, 'qa/runtime/address' + autoload :API, 'qa/runtime/api' end ## @@ -26,6 +29,8 @@ module QA autoload :Group, 'qa/factory/resource/group' autoload :Project, 'qa/factory/resource/project' autoload :DeployKey, 'qa/factory/resource/deploy_key' + autoload :Runner, 'qa/factory/resource/runner' + autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' end module Repository @@ -46,7 +51,7 @@ module QA # autoload :Bootable, 'qa/scenario/bootable' autoload :Actable, 'qa/scenario/actable' - autoload :Entrypoint, 'qa/scenario/entrypoint' + autoload :Taggable, 'qa/scenario/taggable' autoload :Template, 'qa/scenario/template' ## @@ -85,6 +90,7 @@ module QA autoload :Main, 'qa/page/menu/main' autoload :Side, 'qa/page/menu/side' autoload :Admin, 'qa/page/menu/admin' + autoload :Profile, 'qa/page/menu/profile' end module Dashboard @@ -105,8 +111,19 @@ module QA module Settings autoload :Common, 'qa/page/project/settings/common' autoload :Repository, 'qa/page/project/settings/repository' + autoload :CICD, 'qa/page/project/settings/ci_cd' autoload :DeployKeys, 'qa/page/project/settings/deploy_keys' + autoload :Runners, 'qa/page/project/settings/runners' end + + module Pipeline + autoload :Index, 'qa/page/project/pipeline/index' + autoload :Show, 'qa/page/project/pipeline/show' + end + end + + module Profile + autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens' end module Admin @@ -127,10 +144,13 @@ module QA end ## - # Classes describing shell interaction with GitLab + # Classes describing services being part of GitLab and how we can interact + # with these services, like through the shell. # - module Shell - autoload :Omnibus, 'qa/shell/omnibus' + module Service + autoload :Shellout, 'qa/service/shellout' + autoload :Omnibus, 'qa/service/omnibus' + autoload :Runner, 'qa/service/runner' end ## diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb index 7c58e70bcc4..ff0b4a46b77 100644 --- a/qa/qa/factory/resource/deploy_key.rb +++ b/qa/qa/factory/resource/deploy_key.rb @@ -4,6 +4,18 @@ module QA class DeployKey < Factory::Base attr_accessor :title, :key + product :title do + Page::Project::Settings::Repository.act do + expand_deploy_keys(&:key_title) + end + end + + product :fingerprint do + Page::Project::Settings::Repository.act do + expand_deploy_keys(&:key_fingerprint) + end + end + dependency Factory::Resource::Project, as: :project do |project| project.name = 'project-to-deploy' project.description = 'project for adding deploy key test' @@ -13,7 +25,7 @@ module QA project.visit! Page::Menu::Side.act do - click_repository_setting + click_repository_settings end Page::Project::Settings::Repository.perform do |setting| diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb new file mode 100644 index 00000000000..514e3615d18 --- /dev/null +++ b/qa/qa/factory/resource/personal_access_token.rb @@ -0,0 +1,27 @@ +module QA + module Factory + module Resource + ## + # Create a personal access token that can be used by the api + # + class PersonalAccessToken < Factory::Base + attr_accessor :name + + product :access_token do + Page::Profile::PersonalAccessTokens.act { created_access_token } + end + + def fabricate! + Page::Menu::Main.act { go_to_profile_settings } + Page::Menu::Profile.act { click_access_tokens } + + Page::Profile::PersonalAccessTokens.perform do |page| + page.fill_token_name(name || 'api-test-token') + page.check_api + page.create_token + end + end + end + end + end +end diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb new file mode 100644 index 00000000000..5f37f8ac2e9 --- /dev/null +++ b/qa/qa/factory/resource/runner.rb @@ -0,0 +1,42 @@ +require 'securerandom' + +module QA + module Factory + module Resource + class Runner < Factory::Base + attr_writer :name, :tags + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-with-ci-cd' + project.description = 'Project with CI/CD Pipelines' + end + + def name + @name || "qa-runner-#{SecureRandom.hex(4)}" + end + + def tags + @tags || %w[qa e2e] + end + + def fabricate! + project.visit! + + Page::Menu::Side.act { click_ci_cd_settings } + + Service::Runner.new(name).tap do |runner| + Page::Project::Settings::CICD.perform do |settings| + settings.expand_runners_settings do |runners| + runner.pull + runner.token = runners.registration_token + runner.address = runners.coordinator_address + runner.tags = tags + runner.register! + end + end + end + end + end + end + end +end diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md index f72fbfeafca..83710606d7c 100644 --- a/qa/qa/page/README.md +++ b/qa/qa/page/README.md @@ -77,7 +77,7 @@ module Page view 'app/views/devise/sessions/_new_base.html.haml' do element :login_field, 'text_field :login' - element :passowrd_field, 'password_field :password' + element :password_field, 'password_field :password' element :sign_in_button, 'submit "Sign in"' end @@ -103,6 +103,16 @@ view 'app/views/my/view.html.haml' do end ``` +## Running the test locally + +During development, you can run the `qa:selectors` test by running + +```shell +bin/qa Test::Sanity::Selectors +``` + +from within the `qa` directory. + ## Where to ask for help? If you need more information, ask for help on `#qa` channel on Slack (GitLab diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index ea4c920c82c..81ba80cdbaf 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -41,7 +41,21 @@ module QA end def click_element(name) - find(Page::Element.new(name).selector_css).click + find_element(name).click + end + + def find_element(name) + find(element_selector_css(name)) + end + + def within_element(name) + page.within(element_selector_css(name)) do + yield + end + end + + def element_selector_css(name) + Page::Element.new(name).selector_css end def self.path diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 7b4c1603017..9cff2c5c317 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -19,15 +19,17 @@ module QA end def sign_in_using_credentials - if page.has_content?('Change your password') + using_wait_time 0 do + if page.has_content?('Change your password') + fill_in :user_password, with: Runtime::User.password + fill_in :user_password_confirmation, with: Runtime::User.password + click_button 'Change your password' + end + + fill_in :user_login, with: Runtime::User.name fill_in :user_password, with: Runtime::User.password - fill_in :user_password_confirmation, with: Runtime::User.password - click_button 'Change your password' + click_button 'Sign in' end - - fill_in :user_login, with: Runtime::User.name - fill_in :user_password, with: Runtime::User.password - click_button 'Sign in' end def self.path diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/menu/main.rb index f8978b8a5f7..df93a5fa2d2 100644 --- a/qa/qa/page/menu/main.rb +++ b/qa/qa/page/menu/main.rb @@ -7,6 +7,7 @@ module QA element :user_avatar element :user_menu, '.dropdown-menu-nav' element :user_sign_out_link, 'link_to "Sign out"' + element :settings_link, 'link_to "Settings"' end view 'app/views/layouts/nav/_dashboard.html.haml' do @@ -40,7 +41,13 @@ module QA def sign_out within_user_menu do - click_link('Sign out') + click_link 'Sign out' + end + end + + def go_to_profile_settings + within_user_menu do + click_link 'Settings' end end diff --git a/qa/qa/page/menu/profile.rb b/qa/qa/page/menu/profile.rb new file mode 100644 index 00000000000..95e88d863e4 --- /dev/null +++ b/qa/qa/page/menu/profile.rb @@ -0,0 +1,27 @@ +module QA + module Page + module Menu + class Profile < Page::Base + view 'app/views/layouts/nav/sidebar/_profile.html.haml' do + element :access_token_link, 'link_to profile_personal_access_tokens_path' + element :access_token_title, 'Access Tokens' + element :top_level_items, '.sidebar-top-level-items' + end + + def click_access_tokens + within_sidebar do + click_link('Access Tokens') + end + end + + private + + def within_sidebar + page.within('.sidebar-top-level-items') do + yield + end + end + end + end + end +end diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/menu/side.rb index 00b7eba62b0..26f1eccd45c 100644 --- a/qa/qa/page/menu/side.rb +++ b/qa/qa/page/menu/side.rb @@ -5,19 +5,36 @@ module QA view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item element :repository_link, "title: 'Repository'" + element :pipelines_settings_link, "title: 'CI / CD'" element :top_level_items, '.sidebar-top-level-items' element :activity_link, "title: 'Activity'" end - def click_repository_setting - hover_setting do - click_link('Repository') + def click_repository_settings + hover_settings do + within_submenu do + click_link('Repository') + end + end + end + + def click_ci_cd_settings + hover_settings do + within_submenu do + click_link('CI / CD') + end + end + end + + def click_ci_cd_pipelines + within_sidebar do + click_link('CI / CD') end end private - def hover_setting + def hover_settings within_sidebar do find('.qa-settings-item').hover @@ -36,6 +53,12 @@ module QA click_on 'Activity' end end + + def within_submenu + page.within('.fly-out-list') do + yield + end + end end end end diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb new file mode 100644 index 00000000000..f5ae47dadd0 --- /dev/null +++ b/qa/qa/page/profile/personal_access_tokens.rb @@ -0,0 +1,33 @@ +module QA + module Page + module Profile + class PersonalAccessTokens < Page::Base + view 'app/views/shared/_personal_access_tokens_form.html.haml' do + element :personal_access_token_name_field, 'text_field :name' + element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable Lint/InterpolationCheck + element :scopes_api_radios, "label :scopes" + end + + view 'app/views/profiles/personal_access_tokens/index.html.haml' do + element :create_token_field, "text_field_tag 'created-personal-access-token'" + end + + def fill_token_name(name) + fill_in 'personal_access_token_name', with: name + end + + def check_api + check 'personal_access_token_scopes_api' + end + + def create_token + click_on 'Create personal access token' + end + + def created_access_token + page.find('#created-personal-access-token').value + end + end + end + end +end diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb new file mode 100644 index 00000000000..32c108393b9 --- /dev/null +++ b/qa/qa/page/project/pipeline/index.rb @@ -0,0 +1,13 @@ +module QA::Page + module Project::Pipeline + class Index < QA::Page::Base + view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do + element :pipeline_link, 'class="js-pipeline-url-link"' + end + + def go_to_latest_pipeline + first('.js-pipeline-url-link').click + end + end + end +end diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb new file mode 100644 index 00000000000..0835173f1cd --- /dev/null +++ b/qa/qa/page/project/pipeline/show.rb @@ -0,0 +1,35 @@ +module QA::Page + module Project::Pipeline + class Show < QA::Page::Base + view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do + element :pipeline_header, /header class.*ci-header-container.*/ + end + + view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do + element :pipeline_graph, /class.*pipeline-graph.*/ + end + + view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do + element :job_component, /class.*ci-job-component.*/ + end + + view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do + element :status_icon, 'ci-status-icon-${status}' + end + + def running? + within('.ci-header-container') do + return page.has_content?('running') + end + end + + def has_build?(name, status: :success) + within('.pipeline-graph') do + within('.ci-job-component', text: name) do + return has_selector?(".ci-status-icon-#{status}") + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb new file mode 100644 index 00000000000..5270dde7411 --- /dev/null +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -0,0 +1,21 @@ +module QA + module Page + module Project + module Settings + class CICD < Page::Base + include Common + + view 'app/views/projects/settings/ci_cd/show.html.haml' do + element :runners_settings, 'Runners settings' + end + + def expand_runners_settings(&block) + expand_section('Runners settings') do + Settings::Runners.perform(&block) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb index 5d1d5120929..1357bf031d5 100644 --- a/qa/qa/page/project/settings/common.rb +++ b/qa/qa/page/project/settings/common.rb @@ -3,13 +3,23 @@ module QA module Project module Settings module Common - def expand(selector) + def expand(element_name) page.within('#content-body') do - find(selector).click + click_element(element_name) yield end end + + def expand_section(name) + page.within('#content-body') do + page.within('section', text: name) do + click_button 'Expand' + + yield + end + 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 a8d6f09777c..332e84724c7 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -3,12 +3,20 @@ module QA module Project module Settings class DeployKeys < Page::Base - ## - # TODO, define all selectors required by this page object - # - # See gitlab-org/gitlab-qa#154 - # - view 'app/views/projects/deploy_keys/edit.html.haml' + view 'app/views/projects/deploy_keys/_form.html.haml' do + element :deploy_key_title, 'text_field :title' + element :deploy_key_key, 'text_area :key' + end + + view 'app/assets/javascripts/deploy_keys/components/app.vue' do + element :deploy_keys_section, /class=".*deploy\-keys.*"/ + element :project_deploy_keys, 'class="qa-project-deploy-keys"' + end + + view 'app/assets/javascripts/deploy_keys/components/key.vue' do + element :key_title, /class=".*qa-key-title.*"/ + element :key_fingerprint, /class=".*qa-key-fingerprint.*"/ + end def fill_key_title(title) fill_in 'deploy_key_title', with: title @@ -22,9 +30,23 @@ module QA click_on 'Add key' end - def has_key_title?(title) - page.within('.deploy-keys') do - page.find('.title', text: title) + def key_title + within_project_deploy_keys do + find_element(:key_title).text + end + end + + def key_fingerprint + within_project_deploy_keys do + find_element(:key_fingerprint).text + end + end + + private + + def within_project_deploy_keys + within_element(:project_deploy_keys) do + yield end end end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 524d87c6be9..6cc68358c8c 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -5,15 +5,12 @@ module QA class Repository < Page::Base include Common - ## - # TODO, define all selectors required by this page object - # - # See gitlab-org/gitlab-qa#154 - # - view 'app/views/projects/settings/repository/show.html.haml' + view 'app/views/projects/deploy_keys/_index.html.haml' do + element :expand_deploy_keys + end def expand_deploy_keys(&block) - expand('.qa-expand-deploy-keys') do + expand(:expand_deploy_keys) do DeployKeys.perform(&block) end end diff --git a/qa/qa/page/project/settings/runners.rb b/qa/qa/page/project/settings/runners.rb new file mode 100644 index 00000000000..b41668c94cd --- /dev/null +++ b/qa/qa/page/project/settings/runners.rb @@ -0,0 +1,35 @@ +module QA + module Page + module Project + module Settings + class Runners < Page::Base + view 'app/views/ci/runner/_how_to_setup_runner.html.haml' do + element :registration_token, '%code#registration_token' + element :coordinator_address, '%code#coordinator_address' + end + + ## + # TODO, phase-out CSS classes added in Ruby helpers. + # + view 'app/helpers/runners_helper.rb' do + # rubocop:disable Lint/InterpolationCheck + element :runner_status, 'runner-status-#{status}' + # rubocop:enable Lint/InterpolationCheck + end + + def registration_token + find('code#registration_token').text + end + + def coordinator_address + find('code#coordinator_address').text + end + + def has_online_runner? + page.has_css?('.runner-status-online') + end + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index c8af5ba6280..5e66e40a0b5 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -33,6 +33,7 @@ module QA def wait_for_push sleep 5 + refresh end end end diff --git a/qa/qa/runtime/address.rb b/qa/qa/runtime/address.rb new file mode 100644 index 00000000000..ffad3974b02 --- /dev/null +++ b/qa/qa/runtime/address.rb @@ -0,0 +1,20 @@ +module QA + module Runtime + class Address + attr_reader :address + + def initialize(instance, page = nil) + @instance = instance + @address = host + (page.is_a?(String) ? page : page&.path) + end + + def host + if @instance.is_a?(Symbol) + Runtime::Scenario.send("#{@instance}_address") + else + @instance.to_s + end + end + end + end +end diff --git a/qa/qa/runtime/api.rb b/qa/qa/runtime/api.rb new file mode 100644 index 00000000000..e2a096b971d --- /dev/null +++ b/qa/qa/runtime/api.rb @@ -0,0 +1,82 @@ +require 'airborne' + +module QA + module Runtime + module API + class Client + attr_reader :address + + def initialize(address = :gitlab) + @address = address + end + + def personal_access_token + @personal_access_token ||= get_personal_access_token + end + + def get_personal_access_token + # you can set the environment variable PERSONAL_ACCESS_TOKEN + # to use a specific access token rather than create one from the UI + if Runtime::Env.personal_access_token + Runtime::Env.personal_access_token + else + create_personal_access_token + end + end + + private + + def create_personal_access_token + Runtime::Browser.visit(@address, Page::Main::Login) do + Page::Main::Login.act { sign_in_using_credentials } + Factory::Resource::PersonalAccessToken.fabricate!.access_token + end + end + end + + class Request + API_VERSION = 'v4'.freeze + + def initialize(api_client, path, personal_access_token: nil) + personal_access_token ||= api_client.personal_access_token + request_path = request_path(path, personal_access_token: personal_access_token) + @session_address = Runtime::Address.new(api_client.address, request_path) + end + + def url + @session_address.address + end + + # Prepend a request path with the path to the API + # + # path - Path to append + # + # Examples + # + # >> request_path('/issues') + # => "/api/v4/issues" + # + # >> request_path('/issues', personal_access_token: 'sometoken) + # => "/api/v4/issues?private_token=..." + # + # Returns the relative path to the requested API resource + def request_path(path, version: API_VERSION, personal_access_token: nil, oauth_access_token: nil) + full_path = File.join('/api', version, path) + + if oauth_access_token + query_string = "access_token=#{oauth_access_token}" + elsif personal_access_token + query_string = "private_token=#{personal_access_token}" + end + + if query_string + full_path << (path.include?('?') ? '&' : '?') + full_path << query_string + end + + full_path + end + end + end + end +end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 14b2a488760..7b1be3d5ef3 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -24,9 +24,7 @@ module QA # based on `Runtime::Scenario#something_address`. # def visit(address, page, &block) - Browser::Session.new(address, page).tap do |session| - session.perform(&block) - end + Browser::Session.new(address, page).perform(&block) end def self.visit(address, page, &block) @@ -94,20 +92,15 @@ module QA include Capybara::DSL def initialize(instance, page = nil) - @instance = instance - @address = host + page&.path + @session_address = Runtime::Address.new(instance, page) end - def host - if @instance.is_a?(Symbol) - Runtime::Scenario.send("#{@instance}_address") - else - @instance.to_s - end + def url + @session_address.address end def perform(&block) - visit(@address) + visit(url) yield if block_given? rescue @@ -130,7 +123,7 @@ module QA # See gitlab-org/gitlab-qa#102 # def clear! - visit(@address) + visit(url) reset_session! end end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index d5c28e9a7db..56944e8b641 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -3,6 +3,7 @@ module QA module Env extend self + # set to 'false' to have Chrome run visibly instead of headless def chrome_headless? (ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0 end @@ -10,6 +11,11 @@ module QA def running_in_ci? ENV['CI'] || ENV['CI_SERVER'] end + + # specifies token that can be used for the api + def personal_access_token + ENV['PERSONAL_ACCESS_TOKEN'] + end end end end diff --git a/qa/qa/runtime/rsa_key.rb b/qa/qa/runtime/rsa_key.rb new file mode 100644 index 00000000000..d456062bce7 --- /dev/null +++ b/qa/qa/runtime/rsa_key.rb @@ -0,0 +1,21 @@ +require 'net/ssh' +require 'forwardable' + +module QA + module Runtime + class RSAKey + extend Forwardable + + attr_reader :key + def_delegators :@key, :fingerprint + + def initialize(bits = 4096) + @key = OpenSSL::PKey::RSA.new(bits) + end + + def public_key + @public_key ||= "#{key.ssh_type} #{[key.to_blob].pack('m0')}" + end + end + end +end diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb index 2832439d9e0..60027c89ab1 100644 --- a/qa/qa/runtime/user.rb +++ b/qa/qa/runtime/user.rb @@ -10,17 +10,6 @@ module QA def password ENV['GITLAB_PASSWORD'] || '5iveL!fe' end - - def ssh_key - <<~KEY.delete("\n") - ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDFf6RYK3qu/RKF/3ndJmL5xgMLp3O9 - 6x8lTay+QGZ0+9FnnAXMdUqBq/ZU6d/gyMB4IaW3nHzM1w049++yAB6UPCzMB8Uo27K5 - /jyZCtj7Vm9PFNjF/8am1kp46c/SeYicQgQaSBdzIW3UDEa1Ef68qroOlvpi9PYZ/tA7 - M0YP0K5PXX+E36zaIRnJVMPT3f2k+GnrxtjafZrwFdpOP/Fol5BQLBgcsyiU+LM1SuaC - rzd8c9vyaTA1CxrkxaZh+buAi0PmdDtaDrHd42gqZkXCKavyvgM5o2CkQ5LJHCgzpXy0 - 5qNFzmThBSkb+XtoxbyagBiGbVZtSVow6Xa7qewz= dummy@gitlab.com - KEY - end end end end diff --git a/qa/qa/scenario/entrypoint.rb b/qa/qa/scenario/entrypoint.rb deleted file mode 100644 index ae099fd911e..00000000000 --- a/qa/qa/scenario/entrypoint.rb +++ /dev/null @@ -1,34 +0,0 @@ -module QA - module Scenario - ## - # Base class for running the suite against any GitLab instance, - # including staging and on-premises installation. - # - class Entrypoint < Template - include Bootable - - def perform(address, *files) - Runtime::Scenario.define(:gitlab_address, address) - - ## - # Perform before hooks, which are different for CE and EE - # - Runtime::Release.perform_before_hooks - - Specs::Runner.perform do |specs| - specs.tty = true - specs.tags = self.class.get_tags - specs.files = files.any? ? files : 'qa/specs/features' - end - end - - def self.tags(*tags) - @tags = tags - end - - def self.get_tags - @tags - end - end - end -end diff --git a/qa/qa/scenario/taggable.rb b/qa/qa/scenario/taggable.rb new file mode 100644 index 00000000000..b1f24d742e0 --- /dev/null +++ b/qa/qa/scenario/taggable.rb @@ -0,0 +1,17 @@ +module QA + module Scenario + module Taggable + # rubocop:disable Gitlab/ModuleWithInstanceVariables + + def tags(*tags) + @tags = tags + end + + def focus + @tags.to_a + end + + # rubocop:enable Gitlab/ModuleWithInstanceVariables + end + end +end diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb index e2a1f6bf2bd..993bbd723a3 100644 --- a/qa/qa/scenario/test/instance.rb +++ b/qa/qa/scenario/test/instance.rb @@ -2,11 +2,29 @@ module QA module Scenario module Test ## - # Run test suite against any GitLab instance, + # Base class for running the suite against any GitLab instance, # including staging and on-premises installation. # - class Instance < Entrypoint + class Instance < Template + include Bootable + extend Taggable + tags :core + + def perform(address, *files) + Runtime::Scenario.define(:gitlab_address, address) + + ## + # Perform before hooks, which are different for CE and EE + # + Runtime::Release.perform_before_hooks + + Specs::Runner.perform do |specs| + specs.tty = true + specs.tags = self.class.focus + specs.files = files.any? ? files : 'qa/specs/features' + end + end end end end diff --git a/qa/qa/scenario/test/integration/mattermost.rb b/qa/qa/scenario/test/integration/mattermost.rb index 7d0702afdb1..d939f52ab16 100644 --- a/qa/qa/scenario/test/integration/mattermost.rb +++ b/qa/qa/scenario/test/integration/mattermost.rb @@ -6,7 +6,7 @@ module QA # Run test suite against any GitLab instance where mattermost is enabled, # including staging and on-premises installation. # - class Mattermost < Scenario::Entrypoint + class Mattermost < Test::Instance tags :core, :mattermost def perform(address, mattermost, *files) diff --git a/qa/qa/service/omnibus.rb b/qa/qa/service/omnibus.rb new file mode 100644 index 00000000000..b5c06874e5c --- /dev/null +++ b/qa/qa/service/omnibus.rb @@ -0,0 +1,20 @@ +module QA + module Service + class Omnibus + include Scenario::Actable + include Service::Shellout + + def initialize(container) + @name = container + end + + def gitlab_ctl(command, input: nil) + if input.nil? + shell "docker exec #{@name} gitlab-ctl #{command}" + else + shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" + end + end + end + end +end diff --git a/qa/qa/service/runner.rb b/qa/qa/service/runner.rb new file mode 100644 index 00000000000..d0ee33c69f2 --- /dev/null +++ b/qa/qa/service/runner.rb @@ -0,0 +1,41 @@ +require 'securerandom' + +module QA + module Service + class Runner + include Scenario::Actable + include Service::Shellout + + attr_accessor :token, :address, :tags, :image + + def initialize(name) + @image = 'gitlab/gitlab-runner:alpine' + @name = name || "qa-runner-#{SecureRandom.hex(4)}" + @network = Runtime::Scenario.attributes[:network] || 'test' + @tags = %w[qa test] + end + + def pull + shell "docker pull #{@image}" + end + + def register! + shell <<~CMD.tr("\n", ' ') + docker run -d --rm --entrypoint=/bin/sh + --network #{@network} --name #{@name} + -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} + #{@image} -c 'gitlab-runner register && gitlab-runner run' + CMD + end + + def remove! + shell "docker rm -f #{@name}" + end + end + end +end diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb new file mode 100644 index 00000000000..898febde63c --- /dev/null +++ b/qa/qa/service/shellout.rb @@ -0,0 +1,23 @@ +require 'open3' + +module QA + module Service + module Shellout + ## + # TODO, make it possible to use generic QA framework classes + # as a library - gitlab-org/gitlab-qa#94 + # + def shell(command) + puts "Executing `#{command}`" + + Open3.popen2e(command) do |_in, out, wait| + out.each { |line| puts line } + + if wait.value.exited? && wait.value.exitstatus.nonzero? + raise "Command `#{command}` failed!" + end + end + end + end + end +end diff --git a/qa/qa/shell/omnibus.rb b/qa/qa/shell/omnibus.rb deleted file mode 100644 index 6b3628d3109..00000000000 --- a/qa/qa/shell/omnibus.rb +++ /dev/null @@ -1,39 +0,0 @@ -require 'open3' - -module QA - module Shell - class Omnibus - include Scenario::Actable - - def initialize(container) - @name = container - end - - def gitlab_ctl(command, input: nil) - if input.nil? - shell "docker exec #{@name} gitlab-ctl #{command}" - else - shell "docker exec #{@name} bash -c '#{input} | gitlab-ctl #{command}'" - end - end - - private - - ## - # TODO, make it possible to use generic QA framework classes - # as a library - gitlab-org/gitlab-qa#94 - # - def shell(command) - puts "Executing `#{command}`" - - Open3.popen2e(command) do |_in, out, wait| - out.each { |line| puts line } - - if wait.value.exited? && wait.value.exitstatus.nonzero? - raise "Docker command `#{command}` failed!" - end - end - end - end - end -end diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb new file mode 100644 index 00000000000..9d039590a0e --- /dev/null +++ b/qa/qa/specs/features/api/users_spec.rb @@ -0,0 +1,42 @@ +module QA + feature 'API users', :core do + before(:context) do + @api_client = Runtime::API::Client.new(:gitlab) + end + + context 'when authenticated' do + let(:request) { Runtime::API::Request.new(@api_client, '/users') } + + scenario 'get list of users' do + get request.url + + expect_status(200) + end + + scenario 'submit request with a valid user name' do + get request.url, { params: { username: 'root' } } + + expect_status(200) + expect(json_body).to be_an Array + expect(json_body.size).to eq(1) + expect(json_body.first[:username]).to eq Runtime::User.name + end + + scenario 'submit request with an invalid user name' do + get request.url, { params: { username: 'invalid' } } + + expect_status(200) + expect(json_body).to be_an Array + expect(json_body.size).to eq(0) + end + end + + scenario 'submit request with an invalid token' do + request = Runtime::API::Request.new(@api_client, '/users', personal_access_token: 'invalid') + + get request.url + + expect_status(401) + end + end +end diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb index 43a85213501..b9998dda895 100644 --- a/qa/qa/specs/features/project/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb @@ -1,22 +1,20 @@ module QA feature 'deploy keys support', :core do - given(:deploy_key_title) { 'deploy key title' } - given(:deploy_key_value) { Runtime::User.ssh_key } - scenario 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::DeployKey.fabricate! do |deploy_key| - deploy_key.title = deploy_key_title - deploy_key.key = deploy_key_value - end + key = Runtime::RSAKey.new + deploy_key_title = 'deploy key title' + deploy_key_value = key.public_key - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |page| - expect(page).to have_key_title(deploy_key_title) - end + deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| + resource.title = deploy_key_title + resource.key = deploy_key_value end + + expect(deploy_key.title).to eq(deploy_key_title) + expect(deploy_key.fingerprint).to eq(key.fingerprint) end end end diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb new file mode 100644 index 00000000000..1bb7730e06c --- /dev/null +++ b/qa/qa/specs/features/project/pipelines_spec.rb @@ -0,0 +1,102 @@ +module QA + feature 'CI/CD Pipelines', :core, :docker do + let(:executor) { "qa-runner-#{Time.now.to_i}" } + + after do + Service::Runner.new(executor).remove! + end + + scenario 'user registers a new specific runner' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Factory::Resource::Runner.fabricate! do |runner| + runner.name = executor + end + + Page::Project::Settings::CICD.perform do |settings| + sleep 5 # Runner should register within 5 seconds + settings.refresh + + settings.expand_runners_settings do |page| + expect(page).to have_content(executor) + expect(page).to have_online_runner + end + end + end + + scenario 'users creates a new pipeline' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + project = Factory::Resource::Project.fabricate! do |project| + project.name = 'project-with-pipelines' + project.description = 'Project with CI/CD Pipelines.' + end + + Factory::Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = %w[qa test] + end + + Factory::Repository::Push.fabricate! do |push| + push.project = project + push.file_name = '.gitlab-ci.yml' + push.commit_message = 'Add .gitlab-ci.yml' + push.file_content = <<~EOF + test-success: + tags: + - qa + - test + script: echo 'OK' + + test-failure: + tags: + - qa + - test + script: + - echo 'FAILURE' + - exit 1 + + test-tags: + tags: + - qa + - docker + script: echo 'NOOP' + + test-artifacts: + tags: + - qa + - test + script: echo "CONTENTS" > my-artifacts/artifact.txt + artifacts: + paths: + - my-artifacts/ + EOF + end + + Page::Project::Show.act { wait_for_push } + + expect(page).to have_content('Add .gitlab-ci.yml') + + Page::Menu::Side.act { click_ci_cd_pipelines } + + expect(page).to have_content('All 1') + expect(page).to have_content('Add .gitlab-ci.yml') + + puts 'Waiting for the runner to process the pipeline' + sleep 15 # Runner should process all jobs within 15 seconds. + + Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to be_running + expect(pipeline).to have_build('test-success', status: :success) + expect(pipeline).to have_build('test-failure', status: :failed) + expect(pipeline).to have_build('test-tags', status: :pending) + expect(pipeline).to have_build('test-artifacts', status: :failed) + end + end + end +end diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb index 4f6ffe14c9f..51d9c2c7fd2 100644 --- a/qa/qa/specs/features/repository/push_spec.rb +++ b/qa/qa/specs/features/repository/push_spec.rb @@ -11,10 +11,7 @@ module QA push.commit_message = 'Add README.md' end - Page::Project::Show.act do - wait_for_push - refresh - end + Page::Project::Show.act { wait_for_push } expect(page).to have_content('README.md') expect(page).to have_content('This is a test project') diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index 90dd58e20fd..c5663049be8 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -19,7 +19,6 @@ describe QA::Factory::Base do it 'returns fabrication product' do allow(subject).to receive(:new).and_return(factory) - allow(factory).to receive(:fabricate!).and_return('something') result = subject.fabricate!('something') diff --git a/qa/spec/runtime/api_client_spec.rb b/qa/spec/runtime/api_client_spec.rb new file mode 100644 index 00000000000..d497d8839b8 --- /dev/null +++ b/qa/spec/runtime/api_client_spec.rb @@ -0,0 +1,30 @@ +describe QA::Runtime::API::Client do + include Support::StubENV + + describe 'initialization' do + it 'defaults to :gitlab address' do + expect(described_class.new.address).to eq :gitlab + end + + it 'uses specified address' do + client = described_class.new('http:///example.com') + + expect(client.address).to eq 'http:///example.com' + end + end + + describe '#get_personal_access_token' do + it 'returns specified token from env' do + stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + + expect(described_class.new.get_personal_access_token).to eq 'a_token' + end + + it 'returns a created token' do + allow_any_instance_of(described_class) + .to receive(:create_personal_access_token).and_return('created_token') + + expect(described_class.new.get_personal_access_token).to eq 'created_token' + end + end +end diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb new file mode 100644 index 00000000000..9a1ed8a7a46 --- /dev/null +++ b/qa/spec/runtime/api_request_spec.rb @@ -0,0 +1,42 @@ +describe QA::Runtime::API::Request do + include Support::StubENV + + before do + stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + end + + let(:client) { QA::Runtime::API::Client.new('http://example.com') } + let(:request) { described_class.new(client, '/users') } + + describe '#url' do + it 'returns the full api request url' do + expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token' + end + end + + describe '#request_path' do + it 'prepends the api path' do + expect(request.request_path('/users')).to eq '/api/v4/users' + end + + it 'adds the personal access token' do + expect(request.request_path('/users', personal_access_token: 'token')) + .to eq '/api/v4/users?private_token=token' + end + + it 'adds the oauth access token' do + expect(request.request_path('/users', oauth_access_token: 'otoken')) + .to eq '/api/v4/users?access_token=otoken' + end + + it 'respects query parameters' do + expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1' + expect(request.request_path('/users?page=1', personal_access_token: 'token')) + .to eq '/api/v4/users?page=1&private_token=token' + end + + it 'uses a different api version' do + expect(request.request_path('/users', version: 'v3')).to eq '/api/v3/users' + end + end +end diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index 57a72a04507..103573db6be 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -1,7 +1,5 @@ describe QA::Runtime::Env do - before do - allow(ENV).to receive(:[]).and_call_original - end + include Support::StubENV describe '.chrome_headless?' do context 'when there is an env variable set' do @@ -57,8 +55,4 @@ describe QA::Runtime::Env do end end end - - def stub_env(name, value) - allow(ENV).to receive(:[]).with(name).and_return(value) - end end diff --git a/qa/spec/runtime/rsa_key.rb b/qa/spec/runtime/rsa_key.rb new file mode 100644 index 00000000000..ff277b9077b --- /dev/null +++ b/qa/spec/runtime/rsa_key.rb @@ -0,0 +1,9 @@ +describe QA::Runtime::RSAKey do + describe '#public_key' do + subject { described_class.new.public_key } + + it 'generates a public RSA key' do + expect(subject).to match(/\Assh\-rsa AAAA[0-9A-Za-z+\/]+={0,3}\z/) + end + end +end diff --git a/qa/spec/scenario/entrypoint_spec.rb b/qa/spec/scenario/test/instance_spec.rb index aec79dcea04..1824db54c9b 100644 --- a/qa/spec/scenario/entrypoint_spec.rb +++ b/qa/spec/scenario/test/instance_spec.rb @@ -1,6 +1,6 @@ -describe QA::Scenario::Entrypoint do +describe QA::Scenario::Test::Instance do subject do - Class.new(QA::Scenario::Entrypoint) do + Class.new(described_class) do tags :rspec end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 64d06ef6558..c2c6cf95406 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -1,5 +1,7 @@ require_relative '../qa' +Dir[File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } + RSpec.configure do |config| config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true diff --git a/qa/spec/support/stub_env.rb b/qa/spec/support/stub_env.rb new file mode 100644 index 00000000000..bc8f3a5e22e --- /dev/null +++ b/qa/spec/support/stub_env.rb @@ -0,0 +1,38 @@ +# Inspired by https://github.com/ljkbennett/stub_env/blob/master/lib/stub_env/helpers.rb +module Support + module StubENV + def stub_env(key_or_hash, value = nil) + init_stub unless env_stubbed? + + if key_or_hash.is_a? Hash + key_or_hash.each { |k, v| add_stubbed_value(k, v) } + else + add_stubbed_value key_or_hash, value + end + end + + private + + STUBBED_KEY = '__STUBBED__'.freeze + + def add_stubbed_value(key, value) + allow(ENV).to receive(:[]).with(key).and_return(value) + allow(ENV).to receive(:key?).with(key).and_return(true) + allow(ENV).to receive(:fetch).with(key).and_return(value) + allow(ENV).to receive(:fetch).with(key, anything()) do |_, default_val| + value || default_val + end + end + + def env_stubbed? + ENV[STUBBED_KEY] + end + + def init_stub + allow(ENV).to receive(:[]).and_call_original + allow(ENV).to receive(:key?).and_call_original + allow(ENV).to receive(:fetch).and_call_original + add_stubbed_value(STUBBED_KEY, true) + end + end +end |