Merge branch 'qa-123' into 'master'
First iteration to allow creating QA resources using the API See merge request gitlab-org/gitlab-ce!21302
This commit is contained in:
commit
9bf59ceb69
34 changed files with 1250 additions and 209 deletions
1
qa/qa.rb
1
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'
|
||||
|
|
476
qa/qa/factory/README.md
Normal file
476
qa/qa/factory/README.md
Normal file
|
@ -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.
|
97
qa/qa/factory/api_fabricator.rb
Normal file
97
qa/qa/factory/api_fabricator.rb
Normal file
|
@ -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
|
|
@ -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) { [] }
|
||||
|
||||
factory.fabricate!(*args)
|
||||
do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do
|
||||
log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) }
|
||||
|
||||
break Factory::Product.populate!(factory)
|
||||
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?
|
||||
|
||||
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}"
|
||||
|
||||
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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
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
|
||||
def self.populate!(factory, web_url)
|
||||
new(factory, web_url)
|
||||
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
|
||||
|
|
|
@ -7,13 +7,8 @@ module QA
|
|||
project.description = 'Project with repository'
|
||||
end
|
||||
|
||||
product :output do |factory|
|
||||
factory.output
|
||||
end
|
||||
|
||||
product :project do |factory|
|
||||
factory.project
|
||||
end
|
||||
product :output
|
||||
product :project
|
||||
|
||||
def initialize
|
||||
@file_name = 'file.txt'
|
||||
|
|
|
@ -11,7 +11,7 @@ module QA
|
|||
end
|
||||
end
|
||||
|
||||
product(:user) { |factory| factory.user }
|
||||
product :user
|
||||
|
||||
def visit_project_with_retry
|
||||
# The user intermittently fails to stay signed in after visiting the
|
||||
|
|
|
@ -6,6 +6,10 @@ module QA
|
|||
|
||||
dependency Factory::Resource::Sandbox, as: :sandbox
|
||||
|
||||
product :id do
|
||||
true # We don't retrieve the Group ID when using the Browser UI
|
||||
end
|
||||
|
||||
def initialize
|
||||
@path = Runtime::Namespace.name
|
||||
@description = "QA test run at #{Runtime::Namespace.time}"
|
||||
|
@ -35,6 +39,29 @@ module QA
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fabricate_via_api!
|
||||
resource_web_url(api_get)
|
||||
rescue ResourceNotFoundError
|
||||
super
|
||||
end
|
||||
|
||||
def api_get_path
|
||||
"/groups/#{CGI.escape("#{sandbox.path}/#{path}")}"
|
||||
end
|
||||
|
||||
def api_post_path
|
||||
'/groups'
|
||||
end
|
||||
|
||||
def api_post_body
|
||||
{
|
||||
parent_id: sandbox.id,
|
||||
path: path,
|
||||
name: path,
|
||||
visibility: 'public'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -2,16 +2,15 @@ module QA
|
|||
module Factory
|
||||
module Resource
|
||||
class Issue < Factory::Base
|
||||
attr_writer :title, :description, :project
|
||||
attr_accessor :title, :description, :project
|
||||
|
||||
dependency Factory::Resource::Project, as: :project do |project|
|
||||
project.name = 'project-for-issues'
|
||||
project.description = 'project for adding issues'
|
||||
end
|
||||
|
||||
product :title do
|
||||
Page::Project::Issue::Show.act { issue_title }
|
||||
end
|
||||
product :project
|
||||
product :title
|
||||
|
||||
def fabricate!
|
||||
project.visit!
|
||||
|
|
|
@ -12,13 +12,8 @@ module QA
|
|||
:milestone,
|
||||
:labels
|
||||
|
||||
product :project do |factory|
|
||||
factory.project
|
||||
end
|
||||
|
||||
product :source_branch do |factory|
|
||||
factory.source_branch
|
||||
end
|
||||
product :project
|
||||
product :source_branch
|
||||
|
||||
dependency Factory::Resource::Project, as: :project do |project|
|
||||
project.name = 'project-with-merge-request'
|
||||
|
|
|
@ -4,14 +4,13 @@ module QA
|
|||
module Factory
|
||||
module Resource
|
||||
class Project < Factory::Base
|
||||
attr_writer :description
|
||||
attr_accessor :description
|
||||
attr_reader :name
|
||||
|
||||
dependency Factory::Resource::Group, as: :group
|
||||
|
||||
product :name do |factory|
|
||||
factory.name
|
||||
end
|
||||
product :group
|
||||
product :name
|
||||
|
||||
product :repository_ssh_location do
|
||||
Page::Project::Show.act do
|
||||
|
@ -48,6 +47,32 @@ module QA
|
|||
page.create_new_project
|
||||
end
|
||||
end
|
||||
|
||||
def api_get_path
|
||||
"/projects/#{name}"
|
||||
end
|
||||
|
||||
def api_post_path
|
||||
'/projects'
|
||||
end
|
||||
|
||||
def api_post_body
|
||||
{
|
||||
namespace_id: group.id,
|
||||
path: name,
|
||||
name: name,
|
||||
description: description,
|
||||
visibility: 'public'
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def transform_api_resource(resource)
|
||||
resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo])
|
||||
resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo])
|
||||
resource
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,9 +8,7 @@ module QA
|
|||
|
||||
dependency Factory::Resource::Group, as: :group
|
||||
|
||||
product :name do |factory|
|
||||
factory.name
|
||||
end
|
||||
product :name
|
||||
|
||||
def fabricate!
|
||||
group.visit!
|
||||
|
|
|
@ -7,7 +7,7 @@ module QA
|
|||
|
||||
dependency Factory::Resource::Project, as: :project
|
||||
|
||||
product(:title) { |factory| factory.title }
|
||||
product :title
|
||||
|
||||
def title=(title)
|
||||
@title = "#{title}-#{SecureRandom.hex(4)}"
|
||||
|
|
|
@ -6,21 +6,28 @@ module QA
|
|||
# creating it if it doesn't yet exist.
|
||||
#
|
||||
class Sandbox < Factory::Base
|
||||
attr_reader :path
|
||||
|
||||
product :id do
|
||||
true # We don't retrieve the Group ID when using the Browser UI
|
||||
end
|
||||
product :path
|
||||
|
||||
def initialize
|
||||
@name = Runtime::Namespace.sandbox_name
|
||||
@path = Runtime::Namespace.sandbox_name
|
||||
end
|
||||
|
||||
def fabricate!
|
||||
Page::Main::Menu.act { go_to_groups }
|
||||
|
||||
Page::Dashboard::Groups.perform do |page|
|
||||
if page.has_group?(@name)
|
||||
page.go_to_group(@name)
|
||||
if page.has_group?(path)
|
||||
page.go_to_group(path)
|
||||
else
|
||||
page.go_to_new_group
|
||||
|
||||
Page::Group::New.perform do |group|
|
||||
group.set_path(@name)
|
||||
group.set_path(path)
|
||||
group.set_description('GitLab QA Sandbox Group')
|
||||
group.set_visibility('Public')
|
||||
group.create
|
||||
|
@ -28,6 +35,28 @@ module QA
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
def fabricate_via_api!
|
||||
resource_web_url(api_get)
|
||||
rescue ResourceNotFoundError
|
||||
super
|
||||
end
|
||||
|
||||
def api_get_path
|
||||
"/groups/#{path}"
|
||||
end
|
||||
|
||||
def api_post_path
|
||||
'/groups'
|
||||
end
|
||||
|
||||
def api_post_body
|
||||
{
|
||||
path: path,
|
||||
name: path,
|
||||
visibility: 'public'
|
||||
}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -10,17 +10,9 @@ module QA
|
|||
attr_reader :private_key, :public_key, :fingerprint
|
||||
def_delegators :key, :private_key, :public_key, :fingerprint
|
||||
|
||||
product :private_key do |factory|
|
||||
factory.private_key
|
||||
end
|
||||
|
||||
product :title do |factory|
|
||||
factory.title
|
||||
end
|
||||
|
||||
product :fingerprint do |factory|
|
||||
factory.fingerprint
|
||||
end
|
||||
product :private_key
|
||||
product :title
|
||||
product :fingerprint
|
||||
|
||||
def key
|
||||
@key ||= Runtime::Key::RSA.new
|
||||
|
|
|
@ -31,10 +31,10 @@ module QA
|
|||
defined?(@username) && defined?(@password)
|
||||
end
|
||||
|
||||
product(:name) { |factory| factory.name }
|
||||
product(:username) { |factory| factory.username }
|
||||
product(:email) { |factory| factory.email }
|
||||
product(:password) { |factory| factory.password }
|
||||
product :name
|
||||
product :username
|
||||
product :email
|
||||
product :password
|
||||
|
||||
def fabricate!
|
||||
# Don't try to log-out if we're not logged-in
|
||||
|
|
|
@ -10,13 +10,16 @@ module QA
|
|||
end
|
||||
|
||||
def fabricate!
|
||||
Page::Project::Menu.act { click_wiki }
|
||||
Page::Project::Wiki::New.perform do |page|
|
||||
page.go_to_create_first_page
|
||||
page.set_title(@title)
|
||||
page.set_content(@content)
|
||||
page.set_message(@message)
|
||||
page.create_new_page
|
||||
project.visit!
|
||||
|
||||
Page::Project::Menu.perform { |menu_side| menu_side.click_wiki }
|
||||
|
||||
Page::Project::Wiki::New.perform do |wiki_new|
|
||||
wiki_new.go_to_create_first_page
|
||||
wiki_new.set_title(@title)
|
||||
wiki_new.set_content(@content)
|
||||
wiki_new.set_message(@message)
|
||||
wiki_new.create_new_page
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -131,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack
|
|||
(internal, GitLab Team only).
|
||||
|
||||
If you are not a Team Member, and you still need help to contribute, please
|
||||
open an issue in GitLab QA issue tracker.
|
||||
open an issue in GitLab CE issue tracker with the `~QA` label.
|
||||
|
|
|
@ -6,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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -11,9 +11,10 @@ module QA
|
|||
Page::Main::Menu.perform { |main| main.sign_out }
|
||||
Page::Main::Login.act { sign_in_using_credentials }
|
||||
|
||||
Factory::Resource::Project.fabricate! do |resource|
|
||||
project = Factory::Resource::Project.fabricate! do |resource|
|
||||
resource.name = 'add-member-project'
|
||||
end
|
||||
project.visit!
|
||||
|
||||
Page::Project::Menu.act { click_members_settings }
|
||||
Page::Project::Settings::Members.perform do |page|
|
||||
|
|
|
@ -7,17 +7,15 @@ module QA
|
|||
Runtime::Browser.visit(:gitlab, Page::Main::Login)
|
||||
Page::Main::Login.act { sign_in_using_credentials }
|
||||
|
||||
created_project = Factory::Resource::Project.fabricate! do |project|
|
||||
created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project|
|
||||
project.name = 'awesome-project'
|
||||
project.description = 'create awesome project test'
|
||||
end
|
||||
|
||||
expect(created_project.name).to match /^awesome-project-\h{16}$/
|
||||
|
||||
expect(page).to have_content(created_project.name)
|
||||
expect(page).to have_content(
|
||||
/Project \S?awesome-project\S+ was successfully created/
|
||||
)
|
||||
|
||||
expect(page).to have_content('create awesome project test')
|
||||
expect(page).to have_content('The repository for this project is empty')
|
||||
end
|
||||
|
|
|
@ -10,6 +10,7 @@ module QA
|
|||
project = Factory::Resource::Project.fabricate! do |project|
|
||||
project.name = "only-fast-forward"
|
||||
end
|
||||
project.visit!
|
||||
|
||||
Page::Project::Menu.act { go_to_settings }
|
||||
Page::Project::Settings::MergeRequest.act { enable_ff_only }
|
||||
|
|
|
@ -14,10 +14,11 @@ module QA
|
|||
Runtime::Browser.visit(:gitlab, Page::Main::Login)
|
||||
Page::Main::Login.act { sign_in_using_credentials }
|
||||
|
||||
Factory::Resource::Project.fabricate! do |scenario|
|
||||
project = Factory::Resource::Project.fabricate! do |scenario|
|
||||
scenario.name = 'project-with-code'
|
||||
scenario.description = 'project for git clone tests'
|
||||
end
|
||||
project.visit!
|
||||
|
||||
Git::Repository.perform do |repository|
|
||||
repository.uri = location.uri
|
||||
|
|
|
@ -17,6 +17,7 @@ module QA
|
|||
project.name = 'file-template-project'
|
||||
project.description = 'Add file templates via the Web IDE'
|
||||
end
|
||||
@project.visit!
|
||||
|
||||
# Add a file via the regular Files view because the Web IDE isn't
|
||||
# available unless there is a file present
|
||||
|
|
161
qa/spec/factory/api_fabricator_spec.rb
Normal file
161
qa/spec/factory/api_fabricator_spec.rb
Normal file
|
@ -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
|
|
@ -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) }
|
||||
|
||||
before do
|
||||
allow(QA::Factory::Product).to receive(:new).and_return(product)
|
||||
allow(QA::Factory::Product).to receive(:populate!).and_return(product)
|
||||
shared_context 'fabrication context' do
|
||||
subject do
|
||||
Class.new(described_class) do
|
||||
def self.name
|
||||
'MyFactory'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
it 'instantiates the factory and calls factory method' do
|
||||
expect(subject).to receive(:new).and_return(factory)
|
||||
before do
|
||||
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
|
||||
|
||||
subject.fabricate!('something')
|
||||
shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil|
|
||||
let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called }
|
||||
|
||||
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)
|
||||
|
||||
subject.public_send(fabrication_method_called, factory: factory) do |factory|
|
||||
factory.something!
|
||||
end
|
||||
end
|
||||
|
||||
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)
|
||||
|
||||
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
|
||||
|
||||
it 'calls .fabricate_via_browser_ui!' do
|
||||
expect(described_class).to receive(:fabricate_via_browser_ui!)
|
||||
|
||||
described_class.fabricate!
|
||||
end
|
||||
end
|
||||
|
||||
context 'when factory supports fabrication via the API' do
|
||||
it 'calls .fabricate_via_browser_ui!' do
|
||||
expect(described_class).to receive(:fabricate_via_api!)
|
||||
|
||||
described_class.fabricate!
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.fabricate_via_api!' do
|
||||
include_context 'fabrication context'
|
||||
|
||||
it_behaves_like 'fabrication method', :fabricate_via_api!
|
||||
|
||||
it 'instantiates the factory, calls factory method returns fabrication product' do
|
||||
expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
|
||||
|
||||
result = subject.fabricate_via_api!(factory: factory, parents: [])
|
||||
|
||||
expect(result).to eq(product)
|
||||
end
|
||||
|
||||
it 'logs the factory and build method when VERBOSE=true' do
|
||||
stub_env('VERBOSE', 'true')
|
||||
expect(factory).to receive(:fabricate_via_api!).and_return(product_location)
|
||||
|
||||
expect { subject.fabricate_via_api!(factory: factory, parents: []) }
|
||||
.to output(/==> Built a MyFactory via api with args \[\] in [\d\w\.\-]+/)
|
||||
.to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
describe '.fabricate_via_browser_ui!' do
|
||||
include_context 'fabrication context'
|
||||
|
||||
it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate!
|
||||
|
||||
it 'instantiates the factory and calls factory method' do
|
||||
subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
|
||||
|
||||
expect(factory).to have_received(:fabricate!).with('something')
|
||||
end
|
||||
|
||||
it 'returns fabrication product' do
|
||||
allow(subject).to receive(:new).and_return(factory)
|
||||
result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: [])
|
||||
|
||||
result = subject.fabricate!('something')
|
||||
|
||||
expect(result).to eq product
|
||||
expect(result).to eq(product)
|
||||
end
|
||||
|
||||
it 'yields factory before calling factory method' do
|
||||
allow(subject).to receive(:new).and_return(factory)
|
||||
it 'logs the factory and build method when VERBOSE=true' do
|
||||
stub_env('VERBOSE', 'true')
|
||||
|
||||
subject.fabricate! do |factory|
|
||||
factory.something!
|
||||
end
|
||||
|
||||
expect(factory).to have_received(:something!).ordered
|
||||
expect(factory).to have_received(:fabricate!).ordered
|
||||
expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) }
|
||||
.to output(/==> Built a MyFactory via browser_ui with args \["something"\] in [\d\w\.\-]+/)
|
||||
.to_stdout
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -75,9 +152,9 @@ describe QA::Factory::Base do
|
|||
stub_const('Some::MyDependency', dependency)
|
||||
|
||||
allow(subject).to receive(:new).and_return(instance)
|
||||
allow(subject).to receive(:current_url).and_return(product_location)
|
||||
allow(instance).to receive(:mydep).and_return(nil)
|
||||
allow(QA::Factory::Product).to receive(:new)
|
||||
allow(QA::Factory::Product).to receive(:populate!)
|
||||
expect(QA::Factory::Product).to receive(:populate!)
|
||||
end
|
||||
|
||||
it 'builds all dependencies first' do
|
||||
|
@ -89,44 +166,22 @@ describe QA::Factory::Base do
|
|||
end
|
||||
|
||||
describe '.product' do
|
||||
include_context 'fabrication context'
|
||||
|
||||
subject do
|
||||
Class.new(described_class) do
|
||||
def fabricate!
|
||||
"any"
|
||||
end
|
||||
|
||||
# Defined only to be stubbed
|
||||
def self.find_page
|
||||
end
|
||||
|
||||
product :token do
|
||||
find_page.do_something_on_page!
|
||||
'resulting value'
|
||||
end
|
||||
product :token
|
||||
end
|
||||
end
|
||||
|
||||
it 'appends new product attribute' do
|
||||
expect(subject.attributes).to be_one
|
||||
expect(subject.attributes).to have_key(:token)
|
||||
end
|
||||
|
||||
describe 'populating fabrication product with data' do
|
||||
let(:page) { spy('page') }
|
||||
|
||||
before do
|
||||
allow(factory).to receive(:class).and_return(subject)
|
||||
allow(QA::Factory::Product).to receive(:new).and_return(product)
|
||||
allow(product).to receive(:page).and_return(page)
|
||||
allow(subject).to receive(:find_page).and_return(page)
|
||||
end
|
||||
|
||||
it 'populates product after fabrication' do
|
||||
subject.fabricate!
|
||||
|
||||
expect(product.token).to eq 'resulting value'
|
||||
expect(page).to have_received(:do_something_on_page!)
|
||||
end
|
||||
expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
|
||||
expect(subject.attributes[0].name).to eq(:token)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -4,11 +4,11 @@ describe QA::Factory::Dependency do
|
|||
let(:block) { spy('block') }
|
||||
|
||||
let(:signature) do
|
||||
double('signature', factory: dependency, block: block)
|
||||
double('signature', name: :mydep, factory: dependency, block: block)
|
||||
end
|
||||
|
||||
subject do
|
||||
described_class.new(:mydep, factory, signature)
|
||||
described_class.new(factory, signature)
|
||||
end
|
||||
|
||||
describe '#overridden?' do
|
||||
|
@ -55,16 +55,23 @@ describe QA::Factory::Dependency do
|
|||
expect(factory).to have_received(:mydep=).with(dependency)
|
||||
end
|
||||
|
||||
context 'when receives a caller factory as block argument' do
|
||||
let(:dependency) { QA::Factory::Base }
|
||||
it 'calls given block with dependency factory and caller factory' do
|
||||
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
|
||||
|
|
|
@ -1,35 +1,77 @@
|
|||
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
|
||||
|
||||
result = described_class.populate!(factory) do |instance|
|
||||
instance.something = 'string'
|
||||
context 'when the product attribute is populated via a block' do
|
||||
let(:attributes) do
|
||||
[QA::Factory::Product::Attribute.new(:test, proc { 'returned' })]
|
||||
end
|
||||
|
||||
expect(result).to be product
|
||||
expect(result.test).to eq('returned')
|
||||
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
|
||||
|
||||
it 'returns a fabrication product and defines factory attributes as its methods' do
|
||||
expect(factory).to receive(:api_resource).and_return({ test: 'returned' })
|
||||
|
||||
result = described_class.populate!(factory, product_location)
|
||||
|
||||
expect(result).to be_a(described_class)
|
||||
expect(result.test).to eq('returned')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the product attribute is populated via a factory attribute' do
|
||||
let(:attributes) do
|
||||
[QA::Factory::Product::Attribute.new(:foo)]
|
||||
end
|
||||
|
||||
it 'returns a fabrication product and defines factory attributes as its methods' do
|
||||
result = described_class.populate!(factory, product_location)
|
||||
|
||||
expect(result).to be_a(described_class)
|
||||
expect(result.foo).to eq('bar')
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the product attribute has no value' do
|
||||
let(:attributes) do
|
||||
[QA::Factory::Product::Attribute.new(:bar)]
|
||||
end
|
||||
|
||||
it 'returns a fabrication product and defines factory attributes as its methods' do
|
||||
expect { described_class.populate!(factory, product_location) }
|
||||
.to raise_error(described_class::NoValueError, "No value was computed for product bar of factory #{factory.class.name}.")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.visit!' do
|
||||
it 'makes it possible to visit fabrication product' do
|
||||
allow_any_instance_of(described_class)
|
||||
.to receive(:current_url).and_return('some url')
|
||||
allow_any_instance_of(described_class)
|
||||
.to receive(:visit).and_return('visited some url')
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -1,17 +1,23 @@
|
|||
describe QA::Runtime::API::Request do
|
||||
include Support::StubENV
|
||||
|
||||
before do
|
||||
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
|
||||
end
|
||||
|
||||
let(:client) { QA::Runtime::API::Client.new('http://example.com') }
|
||||
let(:request) { described_class.new(client, '/users') }
|
||||
|
||||
before do
|
||||
allow(client).to receive(:personal_access_token).and_return('a_token')
|
||||
end
|
||||
|
||||
describe '#url' do
|
||||
it 'returns the full api request url' do
|
||||
it 'returns the full API request url' do
|
||||
expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token'
|
||||
end
|
||||
|
||||
context 'when oauth_access_token is passed in the query string' do
|
||||
let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) }
|
||||
|
||||
it 'does not adds a private_token query string' do
|
||||
expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#request_path' do
|
||||
|
|
|
@ -34,6 +34,10 @@ describe QA::Runtime::Env do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.verbose?' do
|
||||
it_behaves_like 'boolean method', :verbose?, 'VERBOSE', false
|
||||
end
|
||||
|
||||
describe '.signup_disabled?' do
|
||||
it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false
|
||||
end
|
||||
|
@ -64,7 +68,54 @@ describe QA::Runtime::Env do
|
|||
end
|
||||
end
|
||||
|
||||
describe '.personal_access_token' do
|
||||
around do |example|
|
||||
described_class.instance_variable_set(:@personal_access_token, nil)
|
||||
example.run
|
||||
described_class.instance_variable_set(:@personal_access_token, nil)
|
||||
end
|
||||
|
||||
context 'when PERSONAL_ACCESS_TOKEN is set' do
|
||||
before do
|
||||
stub_env('PERSONAL_ACCESS_TOKEN', 'a_token')
|
||||
end
|
||||
|
||||
it 'returns specified token from env' do
|
||||
expect(described_class.personal_access_token).to eq 'a_token'
|
||||
end
|
||||
end
|
||||
|
||||
context 'when @personal_access_token is set' do
|
||||
before do
|
||||
described_class.personal_access_token = 'another_token'
|
||||
end
|
||||
|
||||
it 'returns the instance variable value' do
|
||||
expect(described_class.personal_access_token).to eq 'another_token'
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '.personal_access_token=' do
|
||||
around do |example|
|
||||
described_class.instance_variable_set(:@personal_access_token, nil)
|
||||
example.run
|
||||
described_class.instance_variable_set(:@personal_access_token, nil)
|
||||
end
|
||||
|
||||
it 'saves the token' do
|
||||
described_class.personal_access_token = 'a_token'
|
||||
|
||||
expect(described_class.personal_access_token).to eq 'a_token'
|
||||
end
|
||||
end
|
||||
|
||||
describe '.forker?' do
|
||||
before do
|
||||
stub_env('GITLAB_FORKER_USERNAME', nil)
|
||||
stub_env('GITLAB_FORKER_PASSWORD', nil)
|
||||
end
|
||||
|
||||
it 'returns false if no forker credentials are defined' do
|
||||
expect(described_class).not_to be_forker
|
||||
end
|
||||
|
|
Loading…
Reference in a new issue