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

First iteration to allow creating QA resources using the API

parent ab9cf561
...@@ -36,6 +36,7 @@ module QA ...@@ -36,6 +36,7 @@ module QA
# GitLab QA fabrication mechanisms # GitLab QA fabrication mechanisms
# #
module Factory module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base' autoload :Base, 'qa/factory/base'
autoload :Dependency, 'qa/factory/dependency' autoload :Dependency, 'qa/factory/dependency'
autoload :Product, 'qa/factory/product' autoload :Product, 'qa/factory/product'
......
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 'forwardable'
require 'capybara/dsl'
module QA module QA
module Factory module Factory
class Base class Base
extend SingleForwardable extend SingleForwardable
include ApiFabricator
extend Capybara::DSL
def_delegators :evaluator, :dependency, :dependencies def_delegators :evaluator, :dependency, :dependencies
def_delegators :evaluator, :product, :attributes def_delegators :evaluator, :product, :attributes
...@@ -12,46 +17,96 @@ module QA ...@@ -12,46 +17,96 @@ module QA
raise NotImplementedError raise NotImplementedError
end end
def self.fabricate!(*args) def self.fabricate!(*args, &prepare_block)
new.tap do |factory| fabricate_via_api!(*args, &prepare_block)
yield factory if block_given? 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| dependencies.each do |signature|
Factory::Dependency.new(name, factory, signature).build! Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
end 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
end end
private_class_method :log_fabrication
def self.evaluator def self.evaluator
@evaluator ||= Factory::Base::DSL.new(self) @evaluator ||= Factory::Base::DSL.new(self)
end end
private_class_method :evaluator
class DSL class DSL
attr_reader :dependencies, :attributes attr_reader :dependencies, :attributes
def initialize(base) def initialize(base)
@base = base @base = base
@dependencies = {} @dependencies = []
@attributes = {} @attributes = []
end end
def dependency(factory, as:, &block) def dependency(factory, as:, &block)
as.tap do |name| as.tap do |name|
@base.class_eval { attr_accessor name } @base.class_eval { attr_accessor name }
Dependency::Signature.new(factory, block).tap do |signature| Dependency::Signature.new(name, factory, block).tap do |signature|
@dependencies.store(name, signature) @dependencies << signature
end end
end end
end end
def product(attribute, &block) def product(attribute, &block)
Product::Attribute.new(attribute, block).tap do |signature| Product::Attribute.new(attribute, block).tap do |signature|
@attributes.store(attribute, signature) @attributes << signature
end end
end end
end end
......
module QA module QA
module Factory module Factory
class Dependency class Dependency
Signature = Struct.new(:factory, :block) Signature = Struct.new(:name, :factory, :block)
def initialize(name, factory, signature) def initialize(caller_factory, dependency_signature)
@name = name @caller_factory = caller_factory
@factory = factory @dependency_signature = dependency_signature
@signature = signature
end end
def overridden? def overridden?
!!@factory.public_send(@name) !!@caller_factory.public_send(@dependency_signature.name)
end end
def build! def build!(parents: [])
return if overridden? return if overridden?
Builder.new(@signature, @factory).fabricate!.tap do |product| dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
@factory.public_send("#{@name}=", product) @dependency_signature.block&.call(factory, @caller_factory)
end
end
class Builder
def initialize(signature, caller_factory)
@factory = signature.factory
@block = signature.block
@caller_factory = caller_factory
end end
def fabricate! dependency.tap do |dependency|
@factory.fabricate! do |factory| @caller_factory.public_send("#{@dependency_signature.name}=", dependency)
@block&.call(factory, @caller_factory)
end
end end
end end
end end
......
...@@ -5,26 +5,46 @@ module QA ...@@ -5,26 +5,46 @@ module QA
class Product class Product
include Capybara::DSL include Capybara::DSL
NoValueError = Class.new(RuntimeError)
attr_reader :factory, :web_url
Attribute = Struct.new(:name, :block) Attribute = Struct.new(:name, :block)
def initialize def initialize(factory, web_url)
@location = current_url @factory = factory
@web_url = web_url
populate_attributes!
end end
def visit! def visit!
visit @location visit(web_url)
end end
def self.populate!(factory) def self.populate!(factory, web_url)
new.tap do |product| new(factory, web_url)
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 }
end end
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 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
end end
end end
...@@ -7,13 +7,8 @@ module QA ...@@ -7,13 +7,8 @@ module QA
project.description = 'Project with repository' project.description = 'Project with repository'
end end
product :output do |factory| product :output
factory.output product :project
end
product :project do |factory|
factory.project
end
def initialize def initialize
@file_name = 'file.txt' @file_name = 'file.txt'
......
...@@ -11,7 +11,7 @@ module QA ...@@ -11,7 +11,7 @@ module QA
end end
end end
product(:user) { |factory| factory.user } product :user
def visit_project_with_retry def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the # The user intermittently fails to stay signed in after visiting the
......
...@@ -6,6 +6,10 @@ module QA ...@@ -6,6 +6,10 @@ module QA
dependency Factory::Resource::Sandbox, as: :sandbox 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 def initialize
@path = Runtime::Namespace.name @path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}" @description = "QA test run at #{Runtime::Namespace.time}"
...@@ -35,6 +39,29 @@ module QA ...@@ -35,6 +39,29 @@ module QA
end end
end 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 end
end end
......
...@@ -2,16 +2,15 @@ module QA ...@@ -2,16 +2,15 @@ module QA
module Factory module Factory
module Resource module Resource
class Issue < Factory::Base class Issue < Factory::Base
attr_writer :title, :description, :project attr_accessor :title, :description, :project
dependency Factory::Resource::Project, as: :project do |project| dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues' project.name = 'project-for-issues'
project.description = 'project for adding issues' project.description = 'project for adding issues'
end end
product :title do product :project
Page::Project::Issue::Show.act { issue_title } product :title
end
def fabricate! def fabricate!
project.visit! project.visit!
......
...@@ -12,13 +12,8 @@ module QA ...@@ -12,13 +12,8 @@ module QA
:milestone, :milestone,
:labels :labels
product :project do |factory| product :project
factory.project product :source_branch
end
product :source_branch do |factory|
factory.source_branch
end
dependency Factory::Resource::Project, as: :project do |project| dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request' project.name = 'project-with-merge-request'
......
...@@ -4,14 +4,13 @@ module QA ...@@ -4,14 +4,13 @@ module QA
module Factory module Factory
module Resource module Resource
class Project < Factory::Base class Project < Factory::Base
attr_writer :description attr_accessor :description
attr_reader :name attr_reader :name
dependency Factory::Resource::Group, as: :group dependency Factory::Resource::Group, as: :group
product :name do |factory| product :group
factory.name product :name
end
product :repository_ssh_location do product :repository_ssh_location do
Page::Project::Show.act do Page::Project::Show.act do
...@@ -48,6 +47,32 @@ module QA ...@@ -48,6 +47,32 @@ module QA
page.create_new_project page.create_new_project
end end
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 end
end end
......
...@@ -8,9 +8,7 @@ module QA ...@@ -8,9 +8,7 @@ module QA
dependency Factory::Resource::Group, as: :group dependency Factory::Resource::Group, as: :group
product :name do |factory| product :name
factory.name
end
def fabricate! def fabricate!
group.visit! group.visit!
......
...@@ -7,7 +7,7 @@ module QA ...@@ -7,7 +7,7 @@ module QA
dependency Factory::Resource::Project, as: :project dependency Factory::Resource::Project, as: :project
product(:title) { |factory| factory.title } product :title
def title=(title) def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}" @title = "#{title}-#{SecureRandom.hex(4)}"
......
...@@ -6,21 +6,28 @@ module QA ...@@ -6,21 +6,28 @@ module QA
# creating it if it doesn't yet exist. # creating it if it doesn't yet exist.
# #
class Sandbox < Factory::Base 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 def initialize
@name = Runtime::Namespace.sandbox_name @path = Runtime::Namespace.sandbox_name
end end
def fabricate! def fabricate!
Page::Main::Menu.act { go_to_groups } Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page| Page::Dashboard::Groups.perform do |page|
if page.has_group?(@name) if page.has_group?(path)
page.go_to_group(@name) page.go_to_group(path)
else else
page.go_to_new_group page.go_to_new_group
Page::Group::New.perform do |group| Page::Group::New.perform do |group|
group.set_path(@name) group.set_path(path)
group.set_description('GitLab QA Sandbox Group') group.set_description('GitLab QA Sandbox Group')
group.set_visibility('Public') group.set_visibility('Public')
group.create group.create
...@@ -28,6 +35,28 @@ module QA ...@@ -28,6 +35,28 @@ module QA
end end
end 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 end
end end
......
...@@ -10,17 +10,9 @@ module QA ...@@ -10,17 +10,9 @@ module QA
attr_reader :private_key, :public_key, :fingerprint attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key do |factory| product :private_key
factory.private_key product :title
end product :fingerprint
product :title do |factory|
factory.title
end
product :fingerprint do |factory|
factory.fingerprint
end
def key def key
@key ||= Runtime::Key::RSA.new @key ||= Runtime::Key::RSA.new
......
...@@ -31,10 +31,10 @@ module QA ...@@ -31,10 +31,10 @@ module QA
defined?(@username) && defined?(@password) defined?(@username) && defined?(@password)
end end
product(:name) { |factory| factory.name } product :name
product(:username) { |factory| factory.username } product :username
product(:email) { |factory| factory.email } product :email
product(:password) { |factory| factory.password } product :password
def fabricate! def fabricate!
# Don't try to log-out if we're not logged-in # Don't try to log-out if we're not logged-in
......
...@@ -10,13 +10,16 @@ module QA ...@@ -10,13 +10,16 @@ module QA
end end
def fabricate! def fabricate!
Page::Project::Menu.act { click_wiki } project.visit!
Page::Project::Wiki::New.perform do |page|
page.go_to_create_first_page Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
page.set_title(@title)
page.set_content(@content) Page::Project::Wiki::New.perform do |wiki_new|
page.set_message(@message) wiki_new.go_to_create_first_page
page.create_new_page wiki_new.set_title(@title)
wiki_new.set_content(@content)
wiki_new.set_message(@message)
wiki_new.create_new_page
end end
end end
end end
......
...@@ -131,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack ...@@ -131,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack
(internal, GitLab Team only). (internal, GitLab Team only).
If you are not a Team Member, and you still need help to contribute, please 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 ...@@ -6,34 +6,35 @@ module QA
class Client class Client
attr_reader :address 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 @address = address
@personal_access_token = personal_access_token @personal_access_token = personal_access_token
@is_new_session = is_new_session
end end
def personal_access_token def personal_access_token
@personal_access_token ||= get_personal_access_token @personal_access_token ||= begin
end
def get_personal_access_token
# you can set the environment variable 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 # to use a specific access token rather than create one from the UI
if Runtime::Env.personal_access_token Runtime::Env.personal_access_token ||= create_personal_access_token
Runtime::Env.personal_access_token
else
create_personal_access_token
end end
end end
private private
def create_personal_access_token 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 } Page::Main::Login.act { sign_in_using_credentials }
Factory::Resource::PersonalAccessToken.fabricate!.access_token Factory::Resource::PersonalAccessToken.fabricate!.access_token
end end
end end
end end
end end
end
end end
...@@ -3,6 +3,12 @@ module QA ...@@ -3,6 +3,12 @@ module QA
module Env module Env
extend self 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 # set to 'false' to have Chrome run visibly instead of headless
def chrome_headless? def chrome_headless?
enabled?(ENV['CHROME_HEADLESS']) enabled?(ENV['CHROME_HEADLESS'])
...@@ -22,7 +28,7 @@ module QA ...@@ -22,7 +28,7 @@ module QA
# specifies token that can be used for the api # specifies token that can be used for the api
def personal_access_token def personal_access_token
ENV['PERSONAL_ACCESS_TOKEN'] @personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN']
end end
def user_username def user_username
...@@ -42,7 +48,7 @@ module QA ...@@ -42,7 +48,7 @@ module QA
end end
def forker? def forker?
forker_username && forker_password !!(forker_username && forker_password)
end end
def forker_username def forker_username
......
...@@ -11,9 +11,10 @@ module QA ...@@ -11,9 +11,10 @@ module QA
Page::Main::Menu.perform { |main| main.sign_out } Page::Main::Menu.perform { |main| main.sign_out }
Page::Main::Login.act { sign_in_using_credentials } 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' resource.name = 'add-member-project'
end end
project.visit!
Page::Project::Menu.act { click_members_settings } Page::Project::Menu.act { click_members_settings }
Page::Project::Settings::Members.perform do |page| Page::Project::Settings::Members.perform do |page|
......
...@@ -7,17 +7,15 @@ module QA ...@@ -7,17 +7,15 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } 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.name = 'awesome-project'
project.description = 'create awesome project test' project.description = 'create awesome project test'
end end
expect(created_project.name).to match /^awesome-project-\h{16}$/ expect(page).to have_content(created_project.name)
expect(page).to have_content( expect(page).to have_content(
/Project \S?awesome-project\S+ was successfully created/ /Project \S?awesome-project\S+ was successfully created/
) )
expect(page).to have_content('create awesome project test') expect(page).to have_content('create awesome project test')
expect(page).to have_content('The repository for this project is empty') expect(page).to have_content('The repository for this project is empty')
end end
......
...@@ -10,6 +10,7 @@ module QA ...@@ -10,6 +10,7 @@ module QA
project = Factory::Resource::Project.fabricate! do |project| project = Factory::Resource::Project.fabricate! do |project|
project.name = "only-fast-forward" project.name = "only-fast-forward"
end end
project.visit!
Page::Project::Menu.act { go_to_settings } Page::Project::Menu.act { go_to_settings }
Page::Project::Settings::MergeRequest.act { enable_ff_only } Page::Project::Settings::MergeRequest.act { enable_ff_only }
......
...@@ -14,10 +14,11 @@ module QA ...@@ -14,10 +14,11 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login) Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials } 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.name = 'project-with-code'
scenario.description = 'project for git clone tests' scenario.description = 'project for git clone tests'
end end
project.visit!
Git::Repository.perform do |repository| Git::Repository.perform do |repository|
repository.uri = location.uri repository.uri = location.uri
......
...@@ -17,6 +17,7 @@ module QA ...@@ -17,6 +17,7 @@ module QA
project.name = 'file-template-project' project.name = 'file-template-project'
project.description = 'Add file templates via the Web IDE' project.description = 'Add file templates via the Web IDE'
end end
@project.visit!
# Add a file via the regular Files view because the Web IDE isn't # Add a file via the regular Files view because the Web IDE isn't
# available unless there is a file present # available unless there is a file present
......
# 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 describe QA::Factory::Base do
include Support::StubENV
let(:factory) { spy('factory') } let(:factory) { spy('factory') }
let(:product) { spy('product') } let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
describe '.fabricate!' do shared_context 'fabrication context' do
subject { Class.new(described_class) } subject do
Class.new(described_class) do
def self.name
'MyFactory'
end
end
end
before do before do
allow(QA::Factory::Product).to receive(:new).and_return(product) allow(subject).to receive(:current_url).and_return(product_location)
allow(QA::Factory::Product).to receive(:populate!).and_return(product) allow(subject).to receive(:new).and_return(factory)
allow(QA::Factory::Product).to receive(:populate!).with(factory, product_location).and_return(product)
end
end end
it 'instantiates the factory and calls factory method' do shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
expect(subject).to receive(:new).and_return(factory) 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 end
it 'returns fabrication product' do it 'does not log the factory and build method when VERBOSE=false' do
allow(subject).to receive(:new).and_return(factory) 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 end
it 'yields factory before calling factory method' do it 'calls .fabricate_via_browser_ui!' do
allow(subject).to receive(:new).and_return(factory) expect(described_class).to receive(:fabricate_via_browser_ui!)
subject.fabricate! do |factory| described_class.fabricate!
factory.something! end
end end
expect(factory).to have_received(:something!).ordered context 'when factory supports fabrication via the API' do
expect(factory).to have_received(:fabricate!).ordered 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
end end
...@@ -75,9 +152,9 @@ describe QA::Factory::Base do ...@@ -75,9 +152,9 @@ describe QA::Factory::Base do
stub_const('Some::MyDependency', dependency) stub_const('Some::MyDependency', dependency)
allow(subject).to receive(:new).and_return(instance) 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(instance).to receive(:mydep).and_return(nil)
allow(QA::Factory::Product).to receive(:new) expect(QA::Factory::Product).to receive(:populate!)
allow(QA::Factory::Product).to receive(:populate!)
end end
it 'builds all dependencies first' do it 'builds all dependencies first' do
...@@ -89,44 +166,22 @@ describe QA::Factory::Base do ...@@ -89,44 +166,22 @@ describe QA::Factory::Base do
end end
describe '.product' do describe '.product' do
include_context 'fabrication context'
subject do subject do
Class.new(described_class) do Class.new(described_class) do
def fabricate! def fabricate!
"any" "any"
end end
# Defined only to be stubbed product :token
def self.find_page
end
product :token do
find_page.do_something_on_page!
'resulting value'
end
end end
end end
it 'appends new product attribute' do it 'appends new product attribute' do
expect(subject.attributes).to be_one expect(subject.attributes).to be_one
expect(subject.attributes).to have_key(:token) expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
end expect(subject.attributes[0].name).to eq(:token)
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
end end
end end
end end
...@@ -4,11 +4,11 @@ describe QA::Factory::Dependency do ...@@ -4,11 +4,11 @@ describe QA::Factory::Dependency do
let(:block) { spy('block') } let(:block) { spy('block') }
let(:signature) do let(:signature) do
double('signature', factory: dependency, block: block) double('signature', name: :mydep, factory: dependency, block: block)
end end
subject do subject do
described_class.new(:mydep, factory, signature) described_class.new(factory, signature)
end end
describe '#overridden?' do describe '#overridden?' do
...@@ -55,16 +55,23 @@ describe QA::Factory::Dependency do ...@@ -55,16 +55,23 @@ describe QA::Factory::Dependency do
expect(factory).to have_received(:mydep=).with(dependency) expect(factory).to have_received(:mydep=).with(dependency)
end 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 it 'calls given block with dependency factory and caller factory' do
allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory) expect(dependency).to receive(:fabricate!).and_yield(dependency)
allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any'))
subject.build! 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 end
end end
......
describe QA::Factory::Product do describe QA::Factory::Product do
let(:factory) do let(:factory) do
QA::Factory::Base.new Class.new(QA::Factory::Base) do
def foo
'bar'
end end
end.new
let(:attributes) do
{ test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) }
end end
let(:product) { spy('product') } let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
subject { described_class.new(factory, product_location) }
describe '.populate!' do
before do before do
allow(QA::Factory::Base).to receive(:attributes).and_return(attributes) expect(factory.class).to receive(:attributes).and_return(attributes)
end end
describe '.populate!' do context 'when the product attribute is populated via a block' do
it 'returns a fabrication product and define factory attributes as its methods' do let(:attributes) do
expect(described_class).to receive(:new).and_return(product) [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| expect(result).to be_a(described_class)
instance.something = 'string' expect(result.test).to eq('returned')
end
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') expect(result.test).to eq('returned')
end end
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 describe '.visit!' do
it 'makes it possible to visit fabrication product' 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) allow_any_instance_of(described_class)
.to receive(:visit).and_return('visited some url') .to receive(:visit).and_return('visited some url')
......
...@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do ...@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do
end end
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 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 end
it 'returns a created token' do it 'returns a created token' do
allow_any_instance_of(described_class) expect(subject).to receive(:create_personal_access_token).and_return('created_token')
.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 end
end end
describe QA::Runtime::API::Request do 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 before do
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') allow(client).to receive(:personal_access_token).and_return('a_token')
end end
let(:client) { QA::Runtime::API::Client.new('http://example.com') }
let(:request) { described_class.new(client, '/users') }
describe '#url' do 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' expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
end 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 end
describe '#request_path' do describe '#request_path' do
......
...@@ -34,6 +34,10 @@ describe QA::Runtime::Env do ...@@ -34,6 +34,10 @@ describe QA::Runtime::Env do
end end
end end
describe '.verbose?' do
it_behaves_like 'boolean method', :verbose?, 'VERBOSE', false
end
describe '.signup_disabled?' do describe '.signup_disabled?' do
it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
end end
...@@ -64,7 +68,54 @@ describe QA::Runtime::Env do ...@@ -64,7 +68,54 @@ describe QA::Runtime::Env do
end 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
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 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 it 'returns false if no forker credentials are defined' do
expect(described_class).not_to be_forker expect(described_class).not_to be_forker
end 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