diff options
-rw-r--r-- | qa/Gemfile | 1 | ||||
-rw-r--r-- | qa/Gemfile.lock | 38 | ||||
-rw-r--r-- | qa/qa.rb | 8 | ||||
-rw-r--r-- | qa/qa/factory/resource/personal_access_token.rb | 27 | ||||
-rw-r--r-- | qa/qa/page/README.md | 12 | ||||
-rw-r--r-- | qa/qa/page/menu/main.rb | 9 | ||||
-rw-r--r-- | qa/qa/page/menu/profile.rb | 27 | ||||
-rw-r--r-- | qa/qa/page/profile/personal_access_tokens.rb | 33 | ||||
-rw-r--r-- | qa/qa/runtime/address.rb | 20 | ||||
-rw-r--r-- | qa/qa/runtime/api.rb | 82 | ||||
-rw-r--r-- | qa/qa/runtime/browser.rb | 19 | ||||
-rw-r--r-- | qa/qa/runtime/env.rb | 6 | ||||
-rw-r--r-- | qa/qa/specs/features/api/users_spec.rb | 42 | ||||
-rw-r--r-- | qa/spec/runtime/api_client_spec.rb | 30 | ||||
-rw-r--r-- | qa/spec/runtime/api_request_spec.rb | 42 | ||||
-rw-r--r-- | qa/spec/runtime/env_spec.rb | 8 | ||||
-rw-r--r-- | qa/spec/spec_helper.rb | 2 | ||||
-rw-r--r-- | qa/spec/support/stub_env.rb | 38 |
18 files changed, 420 insertions, 24 deletions
diff --git a/qa/Gemfile b/qa/Gemfile index 4c866a3f893..d69c71003ae 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -6,3 +6,4 @@ gem 'capybara-screenshot', '~> 1.0.18' gem 'rake', '~> 12.3.0' gem 'rspec', '~> 3.7' gem 'selenium-webdriver', '~> 3.8.0' +gem 'airborne', '~> 0.2.13' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 88d5fe834a0..565adac7499 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,25 @@ 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) + netrc (0.11.0) nokogiri (1.8.1) mini_portile2 (~> 2.3.0) pry (0.11.3) @@ -37,11 +60,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 +81,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,6 +94,7 @@ PLATFORMS ruby DEPENDENCIES + airborne (~> 0.2.13) capybara (~> 2.16.1) capybara-screenshot (~> 1.0.18) pry-byebug (~> 3.5.1) @@ -69,4 +103,4 @@ DEPENDENCIES selenium-webdriver (~> 3.8.0) BUNDLED WITH - 1.16.0 + 1.16.1 @@ -11,6 +11,8 @@ module QA autoload :Scenario, 'qa/runtime/scenario' autoload :Browser, 'qa/runtime/browser' autoload :Env, 'qa/runtime/env' + autoload :Address, 'qa/runtime/address' + autoload :API, 'qa/runtime/api' end ## @@ -26,6 +28,7 @@ module QA autoload :Group, 'qa/factory/resource/group' autoload :Project, 'qa/factory/resource/project' autoload :DeployKey, 'qa/factory/resource/deploy_key' + autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' end module Repository @@ -85,6 +88,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 @@ -108,6 +112,10 @@ module QA end end + module Profile + autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens' + end + module Admin autoload :Settings, 'qa/page/admin/settings' end 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/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/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/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/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/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/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/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 |