Add ability to drive the API in QA specs
This commit is contained in:
parent
fcfda3ef82
commit
b8b9e9eb8e
18 changed files with 420 additions and 24 deletions
|
@ -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'
|
||||
|
|
|
@ -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
|
||||
|
|
8
qa/qa.rb
8
qa/qa.rb
|
@ -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
|
||||
|
|
27
qa/qa/factory/resource/personal_access_token.rb
Normal file
27
qa/qa/factory/resource/personal_access_token.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
27
qa/qa/page/menu/profile.rb
Normal file
27
qa/qa/page/menu/profile.rb
Normal file
|
@ -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
|
33
qa/qa/page/profile/personal_access_tokens.rb
Normal file
33
qa/qa/page/profile/personal_access_tokens.rb
Normal file
|
@ -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
|
20
qa/qa/runtime/address.rb
Normal file
20
qa/qa/runtime/address.rb
Normal file
|
@ -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
|
82
qa/qa/runtime/api.rb
Normal file
82
qa/qa/runtime/api.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
42
qa/qa/specs/features/api/users_spec.rb
Normal file
42
qa/qa/specs/features/api/users_spec.rb
Normal file
|
@ -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
|
30
qa/spec/runtime/api_client_spec.rb
Normal file
30
qa/spec/runtime/api_client_spec.rb
Normal file
|
@ -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
|
42
qa/spec/runtime/api_request_spec.rb
Normal file
42
qa/spec/runtime/api_request_spec.rb
Normal file
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
38
qa/spec/support/stub_env.rb
Normal file
38
qa/spec/support/stub_env.rb
Normal file
|
@ -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
|
Loading…
Reference in a new issue