Add ability to drive the API in QA specs

This commit is contained in:
Brett Walker 2018-01-23 10:23:23 +00:00 committed by Grzegorz Bizon
parent fcfda3ef82
commit b8b9e9eb8e
18 changed files with 420 additions and 24 deletions

View file

@ -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'

View file

@ -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

View file

@ -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

View 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

View file

@ -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

View file

@ -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

View 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

View 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
View 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
View 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

View file

@ -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

View file

@ -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

View 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

View 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

View 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

View file

@ -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

View file

@ -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

View 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