Commit 029c8189 authored by Rémy Coutable's avatar Rémy Coutable Committed by Douglas Barbosa Alexandre

[EE] First iteration to allow creating QA resources using the API

parent c7fce62b
......@@ -11,6 +11,6 @@
.container.blank-state-container
.text-center
= custom_icon("missing_license")
%h4 You do not have a license.
%h4.qa-missing-license You do not have a license.
%p.trial-description You can start a free trial of GitLab Ultimate without any obligation or payment details.
= link_to 'Start free trial', new_trial_url, class: "btn btn-success btn-start-trial prepend-top-10"
......@@ -69,7 +69,7 @@
%p If you remove this license, GitLab will fall back on the previous license, if any.
%p If there is no previous license or if the previous license has expired, some GitLab functionality will be blocked until a new, valid license is uploaded.
%br
= link_to 'Remove license', admin_license_path, data: { confirm: "Are you sure you want to remove the license?" }, method: :delete, class: "btn btn-remove"
= link_to 'Remove license', admin_license_path, data: { confirm: "Are you sure you want to remove the license?" }, method: :delete, class: "btn btn-remove qa-remove-license-link"
= render "breakdown", license: @license
......
......@@ -36,6 +36,7 @@ module QA
# GitLab QA fabrication mechanisms
#
module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base'
autoload :Dependency, 'qa/factory/dependency'
autoload :Product, 'qa/factory/product'
......
......@@ -8,7 +8,7 @@ module QA
QA::Page::Admin::Menu.act { go_to_license }
EE::Page::Admin::License.act(license) do |key|
add_new_license(key) if no_license?
add_new_license(key) unless license?
end
QA::Page::Main::Menu.act { sign_out }
......
......@@ -4,11 +4,12 @@ module QA
module Admin
class License < QA::Page::Base
view 'ee/app/views/admin/licenses/missing.html.haml' do
element :missing_license, 'You do not have a license' # rubocop:disable QA/ElementWithPattern
element :missing_license
end
view 'ee/app/views/admin/licenses/show.html.haml' do
element :license_upload_link, "link_to 'Upload New License'" # rubocop:disable QA/ElementWithPattern
element :remove_license_link
end
view 'ee/app/views/admin/licenses/new.html.haml' do
......@@ -19,8 +20,8 @@ module QA
element :license_upload_buttonm, "submit 'Upload license'" # rubocop:disable QA/ElementWithPattern
end
def no_license?
page.has_content?('You do not have a license')
def license?
has_element?(:remove_license_link) || !has_element?(:missing_license)
end
def add_new_license(key)
......
......@@ -45,6 +45,10 @@ module QA
def initialize
@address = QA::Runtime::Scenario.geo_primary_address
@name = QA::Runtime::Scenario.geo_primary_name
# Alias QA::Runtime::Scenario.gitlab_address to @address since
# some components depends on QA::Runtime::Scenario.gitlab_address.
QA::Runtime::Scenario.define(:gitlab_address, @address)
end
def add_license
......
This diff is collapsed.
# frozen_string_literal: true
require 'airborne'
require 'active_support/core_ext/object/deep_dup'
require 'capybara/dsl'
module QA
module Factory
module ApiFabricator
include Airborne
include Capybara::DSL
HTTP_STATUS_OK = 200
HTTP_STATUS_CREATED = 201
ResourceNotFoundError = Class.new(RuntimeError)
ResourceFabricationFailedError = Class.new(RuntimeError)
ResourceURLMissingError = Class.new(RuntimeError)
attr_reader :api_resource, :api_response
def api_support?
respond_to?(:api_get_path) &&
respond_to?(:api_post_path) &&
respond_to?(:api_post_body)
end
def fabricate_via_api!
unless api_support?
raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!"
end
resource_web_url(api_post)
end
def eager_load_api_client!
api_client.tap do |client|
# Eager-load the API client so that the personal token creation isn't
# taken in account in the actual resource creation timing.
client.personal_access_token
end
end
private
attr_writer :api_resource, :api_response
def resource_web_url(resource)
resource.fetch(:web_url) do
raise ResourceURLMissingError, "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`."
end
end
def api_get
url = Runtime::API::Request.new(api_client, api_get_path).url
response = get(url)
unless response.code == HTTP_STATUS_OK
raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`."
end
process_api_response(parse_body(response))
end
def api_post
response = post(
Runtime::API::Request.new(api_client, api_post_path).url,
api_post_body)
unless response.code == HTTP_STATUS_CREATED
raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`."
end
process_api_response(parse_body(response))
end
def api_client
@api_client ||= begin
Runtime::API::Client.new(:gitlab, is_new_session: !current_url.start_with?('http'))
end
end
def parse_body(response)
JSON.parse(response.body, symbolize_names: true)
end
def process_api_response(parsed_response)
self.api_response = parsed_response
self.api_resource = transform_api_resource(parsed_response.deep_dup)
end
def transform_api_resource(resource)
resource
end
end
end
end
# frozen_string_literal: true
require 'forwardable'
require 'capybara/dsl'
module QA
module Factory
class Base
extend SingleForwardable
include ApiFabricator
extend Capybara::DSL
def_delegators :evaluator, :dependency, :dependencies
def_delegators :evaluator, :product, :attributes
......@@ -12,46 +17,96 @@ module QA
raise NotImplementedError
end
def self.fabricate!(*args)
new.tap do |factory|
yield factory if block_given?
def self.fabricate!(*args, &prepare_block)
fabricate_via_api!(*args, &prepare_block)
rescue NotImplementedError
fabricate_via_browser_ui!(*args, &prepare_block)
end
def self.fabricate_via_browser_ui!(*args, &prepare_block)
options = args.extract_options!
factory = options.fetch(:factory) { new }
parents = options.fetch(:parents) { [] }
do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) }
current_url
end
end
def self.fabricate_via_api!(*args, &prepare_block)
options = args.extract_options!
factory = options.fetch(:factory) { new }
parents = options.fetch(:parents) { [] }
raise NotImplementedError unless factory.api_support?
factory.eager_load_api_client!
do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! }
end
end
def self.do_fabricate!(factory:, prepare_block:, parents: [])
prepare_block.call(factory) if prepare_block
dependencies.each do |name, signature|
Factory::Dependency.new(name, factory, signature).build!
dependencies.each do |signature|
Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
end
factory.fabricate!(*args)
resource_web_url = yield
Factory::Product.populate!(factory, resource_web_url)
end
private_class_method :do_fabricate!
def self.log_fabrication(method, factory, parents, args)
return yield unless Runtime::Env.verbose?
start = Time.now
prefix = "==#{'=' * parents.size}>"
msg = [prefix]
msg << "Built a #{name}"
msg << "as a dependency of #{parents.last}" if parents.any?
msg << "via #{method} with args #{args}"
break Factory::Product.populate!(factory)
yield.tap do
msg << "in #{Time.now - start} seconds"
puts msg.join(' ')
puts if parents.empty?
end
end
private_class_method :log_fabrication
def self.evaluator
@evaluator ||= Factory::Base::DSL.new(self)
end
private_class_method :evaluator
class DSL
attr_reader :dependencies, :attributes
def initialize(base)
@base = base
@dependencies = {}
@attributes = {}
@dependencies = []
@attributes = []
end
def dependency(factory, as:, &block)
as.tap do |name|
@base.class_eval { attr_accessor name }
Dependency::Signature.new(factory, block).tap do |signature|
@dependencies.store(name, signature)
Dependency::Signature.new(name, factory, block).tap do |signature|
@dependencies << signature
end
end
end
def product(attribute, &block)
Product::Attribute.new(attribute, block).tap do |signature|
@attributes.store(attribute, signature)
@attributes << signature
end
end
end
......
module QA
module Factory
class Dependency
Signature = Struct.new(:factory, :block)
Signature = Struct.new(:name, :factory, :block)
def initialize(name, factory, signature)
@name = name
@factory = factory
@signature = signature
def initialize(caller_factory, dependency_signature)
@caller_factory = caller_factory
@dependency_signature = dependency_signature
end
def overridden?
!!@factory.public_send(@name)
!!@caller_factory.public_send(@dependency_signature.name)
end
def build!
def build!(parents: [])
return if overridden?
Builder.new(@signature, @factory).fabricate!.tap do |product|
@factory.public_send("#{@name}=", product)
end
end
class Builder
def initialize(signature, caller_factory)
@factory = signature.factory
@block = signature.block
@caller_factory = caller_factory
dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
@dependency_signature.block&.call(factory, @caller_factory)
end
def fabricate!
@factory.fabricate! do |factory|
@block&.call(factory, @caller_factory)
end
dependency.tap do |dependency|
@caller_factory.public_send("#{@dependency_signature.name}=", dependency)
end
end
end
......
......@@ -3,29 +3,47 @@ require 'capybara/dsl'
module QA
module Factory
class Product
attr_reader :location
include Capybara::DSL
NoValueError = Class.new(RuntimeError)
attr_reader :factory, :web_url
Attribute = Struct.new(:name, :block)
def initialize
@location = current_url
def initialize(factory, web_url)
@factory = factory
@web_url = web_url
populate_attributes!
end
def visit!
visit @location
visit(web_url)
end
def self.populate!(factory, web_url)
new(factory, web_url)
end
def self.populate!(factory)
new.tap do |product|
factory.class.attributes.each_value do |attribute|
product.instance_exec(factory, attribute.block) do |factory, block|
value = block.call(factory)
product.define_singleton_method(attribute.name) { value }
private
def populate_attributes!
factory.class.attributes.each do |attribute|
instance_exec(factory, attribute.block) do |factory, block|
value = attribute_value(attribute, block)
raise NoValueError, "No value was computed for product #{attribute.name} of factory #{factory.class.name}." unless value
define_singleton_method(attribute.name) { value }
end
end
end
def attribute_value(attribute, block)
factory.api_resource&.dig(attribute.name) ||
(block && block.call(factory)) ||
(factory.respond_to?(attribute.name) && factory.public_send(attribute.name))
end
end
end
......
......@@ -7,13 +7,8 @@ module QA
project.description = 'Project with repository'
end
product :output do |factory|
factory.output
end
product :project do |factory|
factory.project
end
product :output
product :project
def initialize
@file_name = 'file.txt'
......
......@@ -11,7 +11,7 @@ module QA
end
end
product(:user) { |factory| factory.user }
product :user
def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the
......
......@@ -6,6 +6,10 @@ module QA
dependency Factory::Resource::Sandbox, as: :sandbox
product :id do
true # We don't retrieve the Group ID when using the Browser UI
end
def initialize
@path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}"
......@@ -35,6 +39,29 @@ module QA
end
end
end
def fabricate_via_api!
resource_web_url(api_get)
rescue ResourceNotFoundError
super
end
def api_get_path
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
end
def api_post_path
'/groups'
end
def api_post_body
{
parent_id: sandbox.id,
path: path,
name: path,
visibility: 'public'
}
end
end
end
end
......
......@@ -2,16 +2,15 @@ module QA
module Factory
module Resource
class Issue < Factory::Base
attr_writer :title, :description, :project
attr_accessor :title, :description, :project
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
end
product :title do
Page::Project::Issue::Show.act { issue_title }
end
product :project
product :title
def fabricate!
project.visit!
......
......@@ -12,13 +12,8 @@ module QA
:milestone,
:labels
product :project do |factory|
factory.project
end
product :source_branch do |factory|
factory.source_branch
end
product :project
product :source_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request'
......
......@@ -4,14 +4,13 @@ module QA
module Factory
module Resource
class Project < Factory::Base
attr_writer :description
attr_accessor :description
attr_reader :name
dependency Factory::Resource::Group, as: :group
product :name do |factory|
factory.name
end
product :group
product :name
product :repository_ssh_location do
Page::Project::Show.act do
......@@ -48,6 +47,32 @@ module QA
page.create_new_project
end
end
def api_get_path
"/projects/#{name}"
end
def api_post_path
'/projects'
end
def api_post_body
{
namespace_id: group.id,
path: name,
name: name,
description: description,
visibility: 'public'
}
end
private
def transform_api_resource(resource)
resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo])
resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo])
resource
end
end
end
end
......
......@@ -8,9 +8,7 @@ module QA
dependency Factory::Resource::Group, as: :group
product :name do |factory|
factory.name
end
product :name
def fabricate!
group.visit!
......
......@@ -7,7 +7,7 @@ module QA
dependency Factory::Resource::Project, as: :project
product(:title) { |factory| factory.title }
product :title
def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}"
......
......@@ -6,21 +6,28 @@ module QA
# creating it if it doesn't yet exist.
#
class Sandbox < Factory::Base
attr_reader :path
product :id do
true # We don't retrieve the Group ID when using the Browser UI
end
product :path
def initialize
@name = Runtime::Namespace.sandbox_name
@path = Runtime::Namespace.sandbox_name
end
def fabricate!
Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
if page.has_group?(@name)
page.go_to_group(@name)
if page.has_group?(path)
page.go_to_group(path)
else
page.go_to_new_group
Page::Group::New.perform do |group|
group.set_path(@name)
group.set_path(path)
group.set_description('GitLab QA Sandbox Group')
group.set_visibility('Public')
group.create
......@@ -28,6 +35,28 @@ module QA
end
end
end
def fabricate_via_api!
resource_web_url(api_get)
rescue ResourceNotFoundError
super
end
def api_get_path
"/groups/#{path}"
end
def api_post_path
'/groups'
end
def api_post_body
{
path: path,
name: path,
visibility: 'public'
}
end
end
end
end
......
......@@ -10,17 +10,9 @@ module QA
attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key do |factory|
factory.private_key
end
product :title do |factory|
factory.title
end
product :fingerprint do |factory|
factory.fingerprint
end
product :private_key
product :title
product :fingerprint
def key
@key ||= Runtime::Key::RSA.new
......
......@@ -31,10 +31,10 @@ module QA
defined?(@username) && defined?(@password)
end
product(:name) { |factory| factory.name }
product(:username) { |factory| factory.username }
product(:email) { |factory| factory.email }
product(:password) { |factory| factory.password }
product :name
product :username
product :email
product :password
def fabricate!
# Don't try to log-out if we're not logged-in
......
......@@ -10,13 +10,16 @@ module QA
end
def fabricate!
Page::Project::Menu.act { click_wiki }
Page::Project::Wiki::New.perform do |page|
page.go_to_create_first_page
page.set_title(@title)
page.set_content(@content)
page.set_message(@message)
page.create_new_page
project.visit!
Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
Page::Project::Wiki::New.perform do |wiki_new|
wiki_new.go_to_create_first_page
wiki_new.set_title(@title)
wiki_new.set_content(@content)
wiki_new.set_message(@message)
wiki_new.create_new_page
end
end
end
......
......@@ -131,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only).
If you are not a Team Member, and you still need help to contribute, please
open an issue in GitLab QA issue tracker.
open an issue in GitLab CE issue tracker with the `~QA` label.
......@@ -6,34 +6,35 @@ module QA
class Client
attr_reader :address
def initialize(address = :gitlab, personal_access_token: nil)
def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true)
@address = address
@personal_access_token = personal_access_token
@is_new_session = is_new_session
end
def personal_access_token
@personal_access_token ||= get_personal_access_token
end
def get_personal_access_token
@personal_access_token ||= begin
# 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
Runtime::Env.personal_access_token ||= create_personal_access_token
end
end
private
def create_personal_access_token
Runtime::Browser.visit(@address, Page::Main::Login) do
if @is_new_session
Runtime::Browser.visit(@address, Page::Main::Login) { do_create_personal_access_token }
else
do_create_personal_access_token
end
end
def do_create_personal_access_token
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::PersonalAccessToken.fabricate!.access_token
end
end
end
end
end
end
......@@ -5,6 +5,12 @@ module QA
extend self
attr_writer :personal_access_token
def verbose?
enabled?(ENV['VERBOSE'], default: false)
end
# set to 'false' to have Chrome run visibly instead of headless
def chrome_headless?
enabled?(ENV['CHROME_HEADLESS'])
......@@ -24,7 +30,7 @@ module QA
# specifies token that can be used for the api
def personal_access_token
ENV['PERSONAL_ACCESS_TOKEN']
@personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN']
end
def user_username
......@@ -44,7 +50,7 @@ module QA
end
def forker?
forker_username && forker_password
!!(forker_username && forker_password)
end
def forker_username
......
......@@ -8,7 +8,7 @@ module QA
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Sandbox.fabricate!
Factory::Resource::Sandbox.fabricate_via_browser_ui!
end
it 'User logs in to group with SAML SSO' do
......
......@@ -11,9 +11,10 @@ module QA
Page::Main::Menu.perform { |main| main.sign_out }
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Project.fabricate! do |resource|
project = Factory::Resource::Project.fabricate! do |resource|
resource.name = 'add-member-project'
end
project.visit!
Page::Project::Menu.act { click_members_settings }
Page::Project::Settings::Members.perform do |page|
......
......@@ -7,17 +7,15 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
created_project = Factory::Resource::Project.fabricate! do |project|
created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project|
project.name = 'awesome-project'
project.description = 'create awesome project test'
end
expect(created_project.name).to match /^awesome-project-\h{16}$/
expect(page).to have_content(created_project.name)
expect(page).to have_content(
/Project \S?awesome-project\S+ was successfully created/
)
expect(page).to have_content('create awesome project test')
expect(page).to have_content('The repository for this project is empty')
end
......
......@@ -3,19 +3,18 @@
module QA
context :plan do
describe 'Epics Creation' do
let!(:issue) do
Factory::Resource::Issue.fabricate! do |issue|
issue.title = 'Issue for epics tests'
end
end
before(:all) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
end
it 'user creates, edits, deletes epic' do
issue = Factory::Resource::Issue.fabricate! do |issue|
issue.title = 'Issue for epics tests'
end
epic = EE::Factory::Resource::Epic.fabricate! do |epic|
epic.group = issue.project.group
epic.title = "My First Epic"
end
......@@ -33,7 +32,7 @@ module QA
# Add/Remove Issues to/from Epics
EE::Page::Group::Epic::Show.perform do |show_page|
show_page.add_issue_to_epic(issue.location)
show_page.add_issue_to_epic(issue.web_url)
show_page.remove_issue_from_epic
expect(show_page).to have_content(/removed issue/)
end
......@@ -47,7 +46,7 @@ module QA
issue.visit!
Page::Project::Issue::Show.perform do |show_page|
show_page.comment("/epic #{epic.location}")
show_page.comment("/epic #{epic.web_url}")
show_page.comment("/remove_epic")
expect(show_page).to have_content(/removed from epic/)
end
......
......@@ -10,6 +10,7 @@ module QA
project = Factory::Resource::Project.fabricate! do |project|
project.name = "only-fast-forward"
end
project.visit!
Page::Project::Menu.act { go_to_settings }
Page::Project::Settings::MergeRequest.act { enable_ff_only }
......
......@@ -14,10 +14,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::Project.fabricate! do |scenario|
project = Factory::Resource::Project.fabricate! do |scenario|
scenario.name = 'project-with-code'
scenario.description = 'project for git clone tests'
end
project.visit!
Git::Repository.perform do |repository|
repository.uri = location.uri
......
......@@ -17,6 +17,7 @@ module QA
project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE'
end
@project.visit!
# Add a file via the regular Files view because the Web IDE isn't
# available unless there is a file present
......
......@@ -19,12 +19,13 @@ module QA
end
# Perform a git push over HTTP directly to the primary
Factory::Repository::Push.fabricate! do |push|
push.repository_http_uri = project.repository_http_location.uri
Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = file_name
push.file_content = "# #{file_content_primary}"
push.commit_message = 'Add README.md'
end
project.visit!
# Validate git push worked and file exists with content
Page::Project::Show.perform do |show|
......
......@@ -12,14 +12,11 @@ module QA
project.description = 'Geo test project'
end
geo_project_name = Page::Project::Show.act { project_name }
expect(geo_project_name).to include 'geo-project'
Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
push.file_name = 'README.md'
push.file_content = '# This is Geo project!'
push.commit_message = 'Add README.md'
push.project = project
end
Runtime::Browser.visit(:geo_secondary, QA::Page::Main::Login) do
......@@ -36,9 +33,9 @@ module QA
end
Page::Dashboard::Projects.perform do |dashboard|
dashboard.wait_for_project_replication(geo_project_name)
dashboard.wait_for_project_replication(project.name)
dashboard.go_to_project(geo_project_name)
dashboard.go_to_project(project.name)
end
Page::Project::Show.perform do |show|
......
# frozen_string_literal: true
describe QA::Factory::ApiFabricator do
let(:factory_without_api_support) do
Class.new do
def self.name
'FooBarFactory'
end
end
end
let(:factory_with_api_support) do
Class.new do
def self.name
'FooBarFactory'
end
def api_get_path
'/foo'
end
def api_post_path
'/bar'
end
def api_post_body
{ name: 'John Doe' }
end
end
end
before do
allow(subject).to receive(:current_url).and_return('')
end
subject { factory.tap { |f| f.include(described_class) }.new }
describe '#api_support?' do
let(:api_client) { spy('Runtime::API::Client') }
let(:api_client_instance) { double('API Client') }
context 'when factory does not support fabrication via the API' do
let(:factory) { factory_without_api_support }
it 'returns false' do
expect(subject).not_to be_api_support
end
end
context 'when factory supports fabrication via the API' do
let(:factory) { factory_with_api_support }
it 'returns false' do
expect(subject).to be_api_support
end
end
end
describe '#fabricate_via_api!' do
let(:api_client) { spy('Runtime::API::Client') }
let(:api_client_instance) { double('API Client') }
before do
stub_const('QA::Runtime::API::Client', api_client)
allow(api_client).to receive(:new).and_return(api_client_instance)
allow(api_client_instance).to receive(:personal_access_token).and_return('foo')
end
context 'when factory does not support fabrication via the API' do
let(:factory) { factory_without_api_support }
it 'raises a NotImplementedError exception' do
expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!")
end
end
context 'when factory supports fabrication via the API' do
let(:factory) { factory_with_api_support }
let(:api_request) { spy('Runtime::API::Request') }
let(:resource_web_url) { 'http://example.org/api/v4/foo' }
let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } }
let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) }
before do
stub_const('QA::Runtime::API::Request', api_request)
allow(api_request).to receive(:new).and_return(double(url: resource_web_url))
end
context 'when creating a resource' do
before do
allow(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
end
it 'returns the resource URL' do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
expect(subject.fabricate_via_api!).to eq(resource_web_url)
end
it 'populates api_resource with the resource' do
subject.fabricate_via_api!
expect(subject.api_resource).to eq(resource)
end
context 'when the POST fails' do
let(:post_response) { { error: "Name already taken." } }
let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json) }
it 'raises a ResourceFabricationFailedError exception' do
expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url))
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.")
expect(subject.api_resource).to be_nil
end
end
end
context '#transform_api_resource' do
let(:factory) do
Class.new do
def self.name
'FooBarFactory'
end
def api_get_path
'/foo'
end
def api_post_path
'/bar'
end
def api_post_body
{ name: 'John Doe' }
end
def transform_api_resource(resource)
resource[:new] = 'foobar'
resource
end
end
end
let(:resource) { { existing: 'foo', web_url: resource_web_url } }
let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } }
it 'transforms the resource' do
expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post)
expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource)
subject.fabricate_via_api!
end
end
end
end
end
# frozen_string_literal: true
describe QA::Factory::Base do
include Support::StubENV
let(:factory) { spy('factory') }
let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
describe '.fabricate!' do
subject { Class.new(described_class) }
shared_context 'fabrication context' do
subject do
Class.new(described_class) do
def self.name
'MyFactory'
end
end
end
before do
allow(QA::Factory::Product).to receive(:new).and_return(product)
allow(QA::Factory::Product).to receive(:populate!).and_return(product)
allow(subject).to receive(:current_url).and_return(product_location)
allow(subject).to receive(:new).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product)
end
end
it 'instantiates the factory and calls factory method' do
expect(subject).to receive(:new).and_return(factory)
shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called }
subject.fabricate!('something')
it 'yields factory before calling factory method' do
expect(factory).to receive(:something!).ordered
expect(factory).to receive(fabrication_method_used).ordered.and_return(product_location)
expect(factory).to have_received(:fabricate!).with('something')
subject.public_send(fabrication_method_called, factory: factory) do |factory|
factory.something!
end
end
it 'returns fabrication product' do
allow(subject).to receive(:new).and_return(factory)
it 'does not log the factory and build method when VERBOSE=false' do
stub_env('VERBOSE', 'false')
expect(factory).to receive(fabrication_method_used).and_return(product_location)
result = subject.fabricate!('something')
expect { subject.public_send(fabrication_method_called, 'something', factory: factory) }
.not_to output.to_stdout
end
end
expect(result).to eq product
describe '.fabricate!' do
context 'when factory does not support fabrication via the API' do
before do
expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError)
end
it 'yields factory before calling factory method' do
allow(subject).to receive(:new).and_return(factory)
it 'calls .fabricate_via_browser_ui!' do
expect(described_class).to receive(:fabricate_via_browser_ui!)
subject.fabricate! do |factory|
factory.something!
described_class.fabricate!
end
end
expect(factory).to have_received(:something!).ordered
expect(factory).to have_received(:fabricate!).ordered
context 'when factory supports fabrication via the API' do
it 'calls .fabricate_via_browser_ui!' do
expect(described_class).to receive(:fabricate_via_api!)
described_class.fabricate!
end
end
end
describe '.fabricate_via_api!' do
include_context 'fabrication context'
it_behaves_like 'fabrication method', :fabricate_via_api!
it 'instantiates the factory, calls factory method returns fabrication product' do
expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
result = subject.fabricate_via_api!(factory: factory, parents: [])
expect(result).to eq(product)
end
it 'logs the factory and build method when VERBOSE=true' do
stub_env('VERBOSE', 'true')
expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
expect { subject.fabricate_via_api!(factory: factory, parents: []) }
.to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/)
.to_stdout
end
end
describe '.fabricate_via_browser_ui!' do
include_context 'fabrication context'
it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
it 'instantiates the factory and calls factory method' do
subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
expect(factory).to have_received(:fabricate!).with('something')
end
it 'returns fabrication product' do
result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
expect(result).to eq(product)
end
it 'logs the factory and build method when VERBOSE=true' do
stub_env('VERBOSE', 'true')
expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) }
.to output(/==> Built a MyFactory via browser_ui with args \["something"\] in [\d\w\.\-]+/)
.to_stdout
end
end
......@@ -75,9 +152,9 @@ describe QA::Factory::Base do
stub_const('Some::MyDependency', dependency)
allow(subject).to receive(:new).and_return(instance)
allow(subject).to receive(:current_url).and_return(product_location)
allow(instance).to receive(:mydep).and_return(nil)
allow(QA::Factory::Product).to receive(:new)
allow(QA::Factory::Product).to receive(:populate!)
expect(QA::Factory::Product).to receive(:populate!)
end
it 'builds all dependencies first' do
......@@ -89,44 +166,22 @@ describe QA::Factory::Base do
end
describe '.product' do
include_context 'fabrication context'
subject do
Class.new(described_class) do
def fabricate!
"any"
end
# Defined only to be stubbed
def self.find_page
end
product :token do
find_page.do_something_on_page!
'resulting value'
end
product :token
end
end
it 'appends new product attribute' do
expect(subject.attributes).to be_one
expect(subject.attributes).to have_key(:token)
end
describe 'populating fabrication product with data' do
let(:page) { spy('page') }
before do
allow(factory).to receive(:class).and_return(subject)
allow(QA::Factory::Product).to receive(:new).and_return(product)
allow(product).to receive(:page).and_return(page)
allow(subject).to receive(:find_page).and_return(page)
end
it 'populates product after fabrication' do
subject.fabricate!
expect(product.token).to eq 'resulting value'
expect(page).to have_received(:do_something_on_page!)
end
expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
expect(subject.attributes[0].name).to eq(:token)
end
end
end
......@@ -4,11 +4,11 @@ describe QA::Factory::Dependency do
let(:block) { spy('block') }
let(:signature) do
double('signature', factory: dependency, block: block)
double('signature', name: :mydep, factory: dependency, block: block)
end
subject do
described_class.new(:mydep, factory, signature)
described_class.new(factory, signature)
end
describe '#overridden?' do
......@@ -55,16 +55,23 @@ describe QA::Factory::Dependency do
expect(factory).to have_received(:mydep=).with(dependency)
end
context 'when receives a caller factory as block argument' do
let(:dependency) { QA::Factory::Base }
it 'calls given block with dependency factory and caller factory' do
allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any'))
expect(dependency).to receive(:fabricate!).and_yield(dependency)
subject.build!
expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory)
expect(block).to have_received(:call).with(dependency, factory)
end
context 'with no block given' do
let(:signature) do
double('signature', name: :mydep, factory: dependency, block: nil)
end
it 'does not error' do
subject.build!
expect(dependency).to have_received(:fabricate!)
end
end
end
......
describe QA::Factory::Product do
let(:factory) do
QA::Factory::Base.new
Class.new(QA::Factory::Base) do
def foo
'bar'
end
let(:attributes) do
{ test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) }
end.new
end
let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
subject { described_class.new(factory, product_location) }
describe '.populate!' do
before do
allow(QA::Factory::Base).to receive(:attributes).and_return(attributes)
expect(factory.class).to receive(:attributes).and_return(attributes)
end
describe '.populate!' do
it 'returns a fabrication product and define factory attributes as its methods' do
expect(described_class).to receive(:new).and_return(product)
context 'when the product attribute is populated via a block' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:test, proc { 'returned' })]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = described_class.populate!(factory, product_location)
result = described_class.populate!(factory) do |instance|
instance.something = 'string'
expect(result).to be_a(described_class)
expect(result.test).to eq('returned')
end
end
expect(result).to be product
context 'when the product attribute is populated via the api' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:test)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
expect(factory).to receive(:api_resource).and_return({ test: 'returned' })
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.test).to eq('returned')
end
end
context 'when the product attribute is populated via a factory attribute' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:foo)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = described_class.populate!(factory, product_location)
expect(result).to be_a(described_class)
expect(result.foo).to eq('bar')
end
end
context 'when the product attribute has no value' do
let(:attributes) do
[QA::Factory::Product::Attribute.new(:bar)]
end
it 'returns a fabrication product and defines factory attributes as its methods' do
expect { described_class.populate!(factory, product_location) }
.to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.")
end
end
end
describe '.visit!' do
it 'makes it possible to visit fabrication product' do
allow_any_instance_of(described_class)
.to receive(:current_url).and_return('some url')
allow_any_instance_of(described_class)
.to receive(:visit).and_return('visited some url')
......
describe QA::Git::Repository do
include Support::StubENV
let(:repository) { described_class.new }
before do
stub_env('GITLAB_USERNAME', 'root')
cd_empty_temp_directory
set_bad_uri
repository.use_default_credentials
......
......@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do
end
end
describe '#get_personal_access_token' do
describe '#personal_access_token' do
context 'when QA::Runtime::Env.personal_access_token is present' do
before do
allow(QA::Runtime::Env).to receive(:personal_access_token).and_return('a_token')
end
it 'returns specified token from env' do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
expect(described_class.new.personal_access_token).to eq 'a_token'
end
end
expect(described_class.new.get_personal_access_token).to eq 'a_token'
context 'when QA::Runtime::Env.personal_access_token is nil' do
before do
allow(QA::Runtime::Env).to receive(:personal_access_token).and_return(nil)
end
it 'returns a created token' do
allow_any_instance_of(described_class)
.to receive(:create_personal_access_token).and_return('created_token')
expect(subject).to receive(:create_personal_access_token).and_return('created_token')
expect(described_class.new.get_personal_access_token).to eq 'created_token'
expect(subject.personal_access_token).to eq 'created_token'
end
end
end
end
describe QA::Runtime::API::Request do
include Support::StubENV
let(:client) { QA::Runtime::API::Client.new('http://example.com') }
let(:request) { described_class.new(client, '/users') }
before do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
allow(client).to receive(:personal_access_token).and_return('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
it 'returns the full API request url' do
expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
end
context 'when oauth_access_token is passed in the query string' do
let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) }
it 'does not adds a private_token query string' do
expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo'
end
end
end
describe '#request_path' do
......
......@@ -34,6 +34,10 @@ describe QA::Runtime::Env do
end
end
describe '.verbose?' do
it_behaves_like 'boolean method', :verbose?, 'VERBOSE', false
end
describe '.signup_disabled?' do
it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
end
......@@ -64,7 +68,54 @@ describe QA::Runtime::Env do
end
end
describe '.personal_access_token' do
around do |example|
described_class.instance_variable_set(:@personal_access_token, nil)
example.run
described_class.instance_variable_set(:@personal_access_token, nil)
end
context 'when PERSONAL_ACCESS_TOKEN is set' do
before do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
end
it 'returns specified token from env' do
expect(described_class.personal_access_token).to eq 'a_token'
end
end
context 'when @personal_access_token is set' do
before do
described_class.personal_access_token = 'another_token'
end
it 'returns the instance variable value' do
expect(described_class.personal_access_token).to eq 'another_token'
end
end
end
describe '.personal_access_token=' do
around do |example|
described_class.instance_variable_set(:@personal_access_token, nil)
example.run
described_class.instance_variable_set(:@personal_access_token, nil)
end
it 'saves the token' do
described_class.personal_access_token = 'a_token'
expect(described_class.personal_access_token).to eq 'a_token'
end
end
describe '.forker?' do
before do
stub_env('GITLAB_FORKER_USERNAME', nil)
stub_env('GITLAB_FORKER_PASSWORD', nil)
end
it 'returns false if no forker credentials are defined' do
expect(described_class).not_to be_forker
end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment