summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--qa/qa.rb1
-rw-r--r--qa/qa/factory/README.md476
-rw-r--r--qa/qa/factory/api_fabricator.rb97
-rw-r--r--qa/qa/factory/base.rb81
-rw-r--r--qa/qa/factory/dependency.rb31
-rw-r--r--qa/qa/factory/product.rb40
-rw-r--r--qa/qa/factory/repository/project_push.rb9
-rw-r--r--qa/qa/factory/resource/fork.rb2
-rw-r--r--qa/qa/factory/resource/group.rb27
-rw-r--r--qa/qa/factory/resource/issue.rb7
-rw-r--r--qa/qa/factory/resource/merge_request.rb9
-rw-r--r--qa/qa/factory/resource/project.rb33
-rw-r--r--qa/qa/factory/resource/project_imported_from_github.rb4
-rw-r--r--qa/qa/factory/resource/project_milestone.rb2
-rw-r--r--qa/qa/factory/resource/sandbox.rb37
-rw-r--r--qa/qa/factory/resource/ssh_key.rb14
-rw-r--r--qa/qa/factory/resource/user.rb8
-rw-r--r--qa/qa/factory/resource/wiki.rb17
-rw-r--r--qa/qa/page/README.md2
-rw-r--r--qa/qa/runtime/api/client.rb29
-rw-r--r--qa/qa/runtime/env.rb10
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb6
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb1
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb1
-rw-r--r--qa/spec/factory/api_fabricator_spec.rb161
-rw-r--r--qa/spec/factory/base_spec.rb149
-rw-r--r--qa/spec/factory/dependency_spec.rb23
-rw-r--r--qa/spec/factory/product_spec.rb74
-rw-r--r--qa/spec/runtime/api/client_spec.rb25
-rw-r--r--qa/spec/runtime/api/request_spec.rb18
-rw-r--r--qa/spec/runtime/api_request_spec.rb0
-rw-r--r--qa/spec/runtime/env_spec.rb51
34 files changed, 1246 insertions, 205 deletions
diff --git a/qa/qa.rb b/qa/qa.rb
index 35ff7458c34..7feca22478a 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -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'
diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md
new file mode 100644
index 00000000000..c56c7c43129
--- /dev/null
+++ b/qa/qa/factory/README.md
@@ -0,0 +1,476 @@
+# Factory objects in GitLab QA
+
+In GitLab QA we are using factories to create resources.
+
+Factories implementation are primarily done using Browser UI steps, but can also
+be done via the API.
+
+## Why do we need that?
+
+We need factory objects because we need to reduce duplication when creating
+resources for our QA tests.
+
+## How to properly implement a factory object?
+
+All factories should inherit from [`Factory::Base`](./base.rb).
+
+There is only one mandatory method to implement to define a factory. This is the
+`#fabricate!` method, which is used to build a resource via the browser UI.
+Note that you should only use [Page objects](../page/README.md) to interact with
+a Web page in this method.
+
+Here is an imaginary example:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attr_accessor :name, :size
+
+ def initialize(name)
+ @name = name
+ end
+
+ def fabricate!
+ Page::Dashboard::Index.perform do |dashboard_index|
+ dashboard_index.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+ end
+ end
+ end
+end
+```
+
+### Define API implementation
+
+A factory may also implement the three following methods to be able to create a
+resource via the public GitLab API:
+
+- `#api_get_path`: The `GET` path to fetch an existing resource.
+- `#api_post_path`: The `POST` path to create a new resource.
+- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource.
+
+Let's take the `Shirt` factory example, and add these three API methods:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attr_accessor :name, :size
+
+ def initialize(name)
+ @name = name
+ end
+
+ def fabricate!
+ Page::Dashboard::Index.perform do |dashboard_index|
+ dashboard_index.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+ end
+end
+```
+
+The [`Project` factory](./resource/project.rb) is a good real example of Browser
+UI and API implementations.
+
+### Define dependencies
+
+A resource may need an other resource to exist first. For instance, a project
+needs a group to be created in.
+
+To define a dependency, you can use the `dependency` DSL method.
+The first argument is a factory class, then you should pass `as: <name>` to give
+a name to the dependency.
+That will allow access to the dependency from your resource object's methods.
+You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`,
+`#api_post_body`.
+
+Let's take the `Shirt` factory, and add a `project` dependency to it:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attr_accessor :name, :size
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-to-create-a-shirt'
+ end
+
+ def initialize(name)
+ @name = name
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/project/#{project.path}/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/project/#{project.path}/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+ end
+end
+```
+
+**Note that dependencies are always built via the API fabrication method if
+supported by their factories.**
+
+### Define attributes on the created resource
+
+Once created, you may want to populate a resource with attributes that can be
+found in the Web page, or in the API response.
+For instance, once you create a project, you may want to store its repository
+SSH URL as an attribute.
+
+To define an attribute, you can use the `product` DSL method.
+The first argument is the attribute name, then you should define a name for the
+dependency to be accessible from your resource object's methods.
+
+Let's take the `Shirt` factory, and define a `:brand` attribute:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attr_accessor :name, :size
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-to-create-a-shirt'
+ end
+
+ # Attribute populated from the Browser UI (using the block)
+ product :brand do
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+ end
+
+ def initialize(name)
+ @name = name
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/project/#{project.path}/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/project/#{project.path}/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+ end
+end
+```
+
+#### Inherit a factory's attribute
+
+Sometimes, you want a resource to inherit its factory attributes. For instance,
+it could be useful to pass the `size` attribute from the `Shirt` factory to the
+created resource.
+You can do that by defining `product :attribute_name` without a block.
+
+Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attr_accessor :name, :size
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-to-create-a-shirt'
+ end
+
+ # Attribute inherited from the Shirt factory if present,
+ # or from the Browser UI otherwise (using the block)
+ product :brand do
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+ end
+
+ # Attribute inherited from the Shirt factory if present,
+ # or a QA::Factory::Product::NoValueError is raised otherwise
+ product :name
+ product :size
+
+ def initialize(name)
+ @name = name
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/project/#{project.path}/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/project/#{project.path}/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+ end
+ end
+ end
+end
+```
+
+#### Define an attribute based on an API response
+
+Sometimes, you want to define a resource attribute based on the API response
+from its `GET` or `POST` request. For instance, if the creation of a shirt via
+the API returns
+
+```ruby
+{
+ brand: 'a-brand-new-brand',
+ size: 'extra-small',
+ style: 't-shirt',
+ materials: [[:cotton, 80], [:polyamide, 20]]
+}
+```
+
+you may want to store `style` as-is in the resource, and fetch the first value
+of the first `materials` item in a `main_fabric` attribute.
+
+For both attributes, you will need to define an inherited attribute, as shown
+in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you
+will need to implement the
+`#transform_api_resource` method to first populate the `:main_fabric` key in the
+API response so that it can be used later to automatically populate the
+attribute on your resource.
+
+If an attribute can only be retrieved from the API response, you should define
+a block to give it a default value, otherwise you could get a
+`QA::Factory::Product::NoValueError` when creating your resource via the
+Browser UI.
+
+Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric`
+attributes:
+
+```ruby
+module QA
+ module Factory
+ module Resource
+ class Shirt < Factory::Base
+ attr_accessor :name, :size
+
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-to-create-a-shirt'
+ end
+
+ # Attribute fetched from the API response if present if present,
+ # or from the Shirt factory if present,
+ # or from the Browser UI otherwise (using the block)
+ product :brand do
+ Page::Shirt::Show.perform do |shirt_show|
+ shirt_show.fetch_brand_from_page
+ end
+ end
+
+ # Attribute fetched from the API response if present if present,
+ # or from the Shirt factory if present,
+ # or a QA::Factory::Product::NoValueError is raised otherwise
+ product :name
+ product :size
+ product :style do
+ 'unknown'
+ end
+ product :main_fabric do
+ 'unknown'
+ end
+
+ def initialize(name)
+ @name = name
+ end
+
+ def fabricate!
+ project.visit!
+
+ Page::Project::Show.perform do |project_show|
+ project_show.go_to_new_shirt
+ end
+
+ Page::Shirt::New.perform do |shirt_new|
+ shirt_new.set_name(name)
+ shirt_new.create_shirt!
+ end
+ end
+
+ def api_get_path
+ "/project/#{project.path}/shirt/#{name}"
+ end
+
+ def api_post_path
+ "/project/#{project.path}/shirts"
+ end
+
+ def api_post_body
+ {
+ name: name
+ }
+ end
+
+ private
+
+ def transform_api_resource(api_response)
+ api_response[:main_fabric] = api_response[:materials][0][0]
+ api_response
+ end
+ end
+ end
+ end
+end
+```
+
+**Notes on attributes precedence:**
+
+- attributes from the API response take precedence over attributes from the
+ factory (i.e inherited)
+- attributes from the factory (i.e inherited) take precedence over attributes
+ from the Browser UI
+- attributes without a value will raise a `QA::Factory::Product::NoValueError` error
+
+## Creating resources in your tests
+
+To create a resource in your tests, you can call the `.fabricate!` method on the
+factory class.
+Note that if the factory supports API fabrication, this will use this
+fabrication by default.
+
+Here is an example that will use the API fabrication method under the hood since
+it's supported by the `Shirt` factory:
+
+```ruby
+my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt|
+ shirt.size = 'small'
+end
+
+expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
+expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response
+expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response
+expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response
+```
+
+If you explicitely want to use the Browser UI fabrication method, you can call
+the `.fabricate_via_browser_ui!` method instead:
+
+```ruby
+my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt|
+ shirt.size = 'small'
+end
+
+expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page
+expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute
+expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute
+expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block
+expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block
+```
+
+You can also explicitely use the API fabrication method, by calling the
+`.fabricate_via_api!` method:
+
+```ruby
+my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt|
+ shirt.size = 'small'
+end
+```
+
+In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`.
+
+## Where to ask for help?
+
+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 CE issue tracker with the `~QA` label.
diff --git a/qa/qa/factory/api_fabricator.rb b/qa/qa/factory/api_fabricator.rb
new file mode 100644
index 00000000000..b1cfb6c9783
--- /dev/null
+++ b/qa/qa/factory/api_fabricator.rb
@@ -0,0 +1,97 @@
+# 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
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index 7a532ce534b..a8ecac2a1e6 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -1,9 +1,14 @@
+# 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
- dependencies.each do |name, signature|
- Factory::Dependency.new(name, factory, signature).build!
- 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 |signature|
+ Factory::Dependency.new(factory, signature).build!(parents: parents + [self])
+ end
+
+ 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?
- factory.fabricate!(*args)
+ 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
diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb
index fc5dc82ce29..655e2677db0 100644
--- a/qa/qa/factory/dependency.rb
+++ b/qa/qa/factory/dependency.rb
@@ -1,37 +1,26 @@
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
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
index 996b7f14f61..17fe908eaa2 100644
--- a/qa/qa/factory/product.rb
+++ b/qa/qa/factory/product.rb
@@ -5,26 +5,46 @@ module QA
class Product
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 }
- 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
+
+ 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
diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb
index 167f47c9141..6f878396f0e 100644
--- a/qa/qa/factory/repository/project_push.rb
+++ b/qa/qa/factory/repository/project_push.rb
@@ -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'
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
index 83dd4000f0a..6e2a668df64 100644
--- a/qa/qa/factory/resource/fork.rb
+++ b/qa/qa/factory/resource/fork.rb
@@ -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
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
index 033fc48c08f..2688328df92 100644
--- a/qa/qa/factory/resource/group.rb
+++ b/qa/qa/factory/resource/group.rb
@@ -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
diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb
index 95f48e20b3e..9b444cb0bf1 100644
--- a/qa/qa/factory/resource/issue.rb
+++ b/qa/qa/factory/resource/issue.rb
@@ -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!
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index c4620348202..18046c7a8b2 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -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'
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index 90db26ab3ab..105e42b23ec 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -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
diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb
index df2a3340d60..a45e7fee03b 100644
--- a/qa/qa/factory/resource/project_imported_from_github.rb
+++ b/qa/qa/factory/resource/project_imported_from_github.rb
@@ -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!
diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb
index 1251ae03135..35383842142 100644
--- a/qa/qa/factory/resource/project_milestone.rb
+++ b/qa/qa/factory/resource/project_milestone.rb
@@ -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)}"
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
index 5249e1755a6..e592f4e0dd2 100644
--- a/qa/qa/factory/resource/sandbox.rb
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -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
diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb
index 45236f69de9..a512d071dd4 100644
--- a/qa/qa/factory/resource/ssh_key.rb
+++ b/qa/qa/factory/resource/ssh_key.rb
@@ -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
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
index e8b9ea2e6b4..36edf787b64 100644
--- a/qa/qa/factory/resource/user.rb
+++ b/qa/qa/factory/resource/user.rb
@@ -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
diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb
index acfe143fa61..d697433736e 100644
--- a/qa/qa/factory/resource/wiki.rb
+++ b/qa/qa/factory/resource/wiki.rb
@@ -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
diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md
index 4d58f1a43b7..d0de33892c4 100644
--- a/qa/qa/page/README.md
+++ b/qa/qa/page/README.md
@@ -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.
diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb
index 02015e23ad8..0545b500e4c 100644
--- a/qa/qa/runtime/api/client.rb
+++ b/qa/qa/runtime/api/client.rb
@@ -6,33 +6,34 @@ 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
- # 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
+ @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
+ 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
- Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::PersonalAccessToken.fabricate!.access_token
+ 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
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 4a2109799fa..533ed87453a 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -3,6 +3,12 @@ module QA
module Env
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'])
@@ -22,7 +28,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
@@ -42,7 +48,7 @@ module QA
end
def forker?
- forker_username && forker_password
+ !!(forker_username && forker_password)
end
def forker_username
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
index b276c7ee579..53865b44684 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb
@@ -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|
diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
index bb1f3ab26d1..c8ea558aed6 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb
@@ -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
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
index 984cea8ca10..827dbb67076 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb
@@ -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 }
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
index 0dcdc6639d1..a982a4604ac 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb
@@ -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
diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
index ab5d97d5b66..1f07d08e664 100644
--- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb
@@ -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
diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/factory/api_fabricator_spec.rb
new file mode 100644
index 00000000000..e5fbc064911
--- /dev/null
+++ b/qa/spec/factory/api_fabricator_spec.rb
@@ -0,0 +1,161 @@
+# 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
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
index 04e04886699..184802a7903 100644
--- a/qa/spec/factory/base_spec.rb
+++ b/qa/spec/factory/base_spec.rb
@@ -1,40 +1,117 @@
+# 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
+
+ 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
- expect(result).to eq product
+ it 'calls .fabricate_via_browser_ui!' do
+ expect(described_class).to receive(:fabricate_via_browser_ui!)
+
+ described_class.fabricate!
+ end
end
- it 'yields factory before calling factory method' do
- allow(subject).to receive(:new).and_return(factory)
+ context 'when factory supports fabrication via the API' do
+ it 'calls .fabricate_via_browser_ui!' do
+ expect(described_class).to receive(:fabricate_via_api!)
- subject.fabricate! do |factory|
- factory.something!
+ 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)
- expect(factory).to have_received(:something!).ordered
- expect(factory).to have_received(:fabricate!).ordered
+ 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
diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb
index 8aaa6665a18..657beddffb1 100644
--- a/qa/spec/factory/dependency_spec.rb
+++ b/qa/spec/factory/dependency_spec.rb
@@ -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
+ expect(dependency).to receive(:fabricate!).and_yield(dependency)
- 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'))
+ subject.build!
+
+ 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(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory)
+ expect(dependency).to have_received(:fabricate!)
end
end
end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
index f245aabbf43..43b1d93d769 100644
--- a/qa/spec/factory/product_spec.rb
+++ b/qa/spec/factory/product_spec.rb
@@ -1,36 +1,78 @@
describe QA::Factory::Product do
let(:factory) do
- QA::Factory::Base.new
- end
-
- let(:attributes) do
- { test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) }
+ Class.new(QA::Factory::Base) do
+ def foo
+ 'bar'
+ end
+ end.new
end
let(:product) { spy('product') }
+ let(:product_location) { 'http://product_location' }
- before do
- allow(QA::Factory::Base).to receive(:attributes).and_return(attributes)
- end
+ subject { described_class.new(factory, product_location) }
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)
+ before do
+ expect(factory.class).to receive(:attributes).and_return(attributes)
+ end
+
+ 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)
+
+ expect(result).to be_a(described_class)
+ expect(result.test).to eq('returned')
+ end
+ end
+
+ context 'when the product attribute is populated via the api' do
+ let(:attributes) do
+ [QA::Factory::Product::Attribute.new(:test)]
+ end
- result = described_class.populate!(factory) do |instance|
- instance.something = 'string'
+ 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
- expect(result).to be product
- expect(result.test).to eq('returned')
+ 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')
expect(subject.visit!).to eq 'visited some url'
diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb
index d497d8839b8..975586b505f 100644
--- a/qa/spec/runtime/api/client_spec.rb
+++ b/qa/spec/runtime/api/client_spec.rb
@@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do
end
end
- describe '#get_personal_access_token' do
- it 'returns specified token from env' do
- stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
+ 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
- expect(described_class.new.get_personal_access_token).to eq 'a_token'
+ it 'returns specified token from env' do
+ expect(described_class.new.personal_access_token).to eq 'a_token'
+ end
end
- it 'returns a created token' do
- allow_any_instance_of(described_class)
- .to receive(:create_personal_access_token).and_return('created_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
- expect(described_class.new.get_personal_access_token).to eq 'created_token'
+ it 'returns a created token' do
+ expect(subject).to receive(:create_personal_access_token).and_return('created_token')
+
+ expect(subject.personal_access_token).to eq 'created_token'
+ end
end
end
end
diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb
index 80e3149f32d..08233e3c1d6 100644
--- a/qa/spec/runtime/api/request_spec.rb
+++ b/qa/spec/runtime/api/request_spec.rb
@@ -1,17 +1,23 @@
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
diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb
deleted file mode 100644
index e69de29bb2d..00000000000
--- a/qa/spec/runtime/api_request_spec.rb
+++ /dev/null
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index fda955f6600..b5ecf1afb80 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -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