Commit 51518019 authored by Lin Jen-Shin's avatar Lin Jen-Shin Committed by Douglas Barbosa Alexandre

Always use `attribute` to define the product

parent bf96ec85
......@@ -39,7 +39,6 @@ module QA
module Factory
autoload :ApiFabricator, 'qa/factory/api_fabricator'
autoload :Base, 'qa/factory/base'
autoload :Dependency, 'qa/factory/dependency'
autoload :Product, 'qa/factory/product'
module Resource
......
......@@ -26,11 +26,7 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
def initialize(name)
@name = name
end
attr_accessor :name
def fabricate!
Page::Dashboard::Index.perform do |dashboard_index|
......@@ -64,21 +60,10 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
def initialize(name)
@name = name
end
attr_accessor :name
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
# ... same as before
end
def api_get_path
......@@ -103,33 +88,69 @@ end
The [`Project` factory](./resource/project.rb) is a good real example of Browser
UI and API implementations.
### Define dependencies
### Define attributes
After the resource is fabricated, we would like to access the attributes on
the resource. We define the attributes with `attribute` method. Suppose
we want to access the name on the resource, we could change `attr_accessor`
to `attribute`:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attribute :name
A resource may need an other resource to exist first. For instance, a project
# ... same as before
end
end
end
end
```
The difference between `attr_accessor` and `attribute` is that by using
`attribute` it can also be accessed from the product:
```ruby
shirt =
QA::Factory::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.name # => "GitLab QA"
```
In the above example, if we use `attr_accessor :name` then `shirt.name` won't
be available. On the other hand, using `attribute :name` will allow you to use
`shirt.name`, so most of the time you'll want to use `attribute` instead of
`attr_accessor` unless we clearly don't need it for the product.
#### Resource attributes
A resource may need another 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`.
To define a resource attribute, you can use the `attribute` method with a
block using the other factory to fabricate the resource.
Let's take the `Shirt` factory, and add a `project` dependency to it:
That will allow access to the other resource 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` attribute to it:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
attribute :name
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
end
def initialize(name)
@name = name
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-create-a-shirt'
end
end
def fabricate!
......@@ -164,19 +185,19 @@ module QA
end
```
**Note that dependencies are always built via the API fabrication method if
supported by their factories.**
**Note that all the attributes are lazily constructed. This means if you want
a specific attribute to be fabricated first, you'll need to call the
attribute method first even if you're not using it.**
### Define attributes on the created resource
#### Product data attributes
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.
Again we could use the `attribute` method with a block, using a page object
to retrieve the data on the page.
Let's take the `Shirt` factory, and define a `:brand` attribute:
......@@ -185,22 +206,74 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
attribute :name
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-create-a-shirt'
end
end
# Attribute populated from the Browser UI (using the block)
product :brand do
attribute :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
def initialize(name)
@name = name
end
# ... same as before
end
end
end
end
```
**Note again that all the attributes are lazily constructed. This means if
you call `shirt.brand` after moving to the other page, it'll not properly
retrieve the data because we're no longer on the expected page.**
Consider this:
```ruby
shirt =
QA::Factory::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.project.visit!
shirt.brand # => FAIL!
```
The above example will fail because now we're on the project page, trying to
construct the brand data from the shirt page, however we moved to the project
page already. There are two ways to solve this, one is that we could try to
retrieve the brand before visiting the project again:
```ruby
shirt =
QA::Factory::Resource::Shirt.fabricate! do |resource|
resource.name = "GitLab QA"
end
shirt.brand # => OK!
shirt.project.visit!
shirt.brand # => OK!
```
The attribute will be stored in the instance therefore all the following calls
will be fine, using the data previously constructed. If we think that this
might be too brittle, we could eagerly construct the data right before
ending fabrication:
```ruby
module QA
module Factory
module Resource
class Shirt < Factory::Base
# ... same as before
def fabricate!
project.visit!
......@@ -213,20 +286,8 @@ module QA
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
}
brand # Eagerly construct the data
end
end
end
......@@ -234,74 +295,48 @@ module QA
end
```
#### Inherit a factory's attribute
This will make sure we construct the data right after we created the shirt.
The drawback for this will become we're forced to construct the data even
if we don't really need to use it.
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:
Alternatively, we could just make sure we're on the right page before
constructing the brand data:
```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 :name
# Attribute from the Browser UI (using the block)
product :brand do
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-create-a-shirt'
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
# Attribute populated from the Browser UI (using the block)
attribute :brand do
back_url = current_url
visit!
Page::Shirt::New.perform do |shirt_new|
shirt_new.set_name(name)
shirt_new.create_shirt!
Page::Shirt::Show.perform do |shirt_show|
shirt_show.fetch_brand_from_page
end
end
def api_get_path
"/project/#{project.path}/shirt/#{name}"
visit(back_url)
end
def api_post_path
"/project/#{project.path}/shirts"
end
def api_post_body
{
name: name
}
end
# ... same as before
end
end
end
end
```
This will make sure it's on the shirt page before constructing brand, and
move back to the previous page to avoid breaking the state.
#### Define an attribute based on an API response
Sometimes, you want to define a resource attribute based on the API response
......@@ -311,7 +346,6 @@ the API returns
```ruby
{
brand: 'a-brand-new-brand',
size: 'extra-small',
style: 't-shirt',
materials: [[:cotton, 80], [:polyamide, 20]]
}
......@@ -320,18 +354,6 @@ the API returns
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:
......@@ -340,69 +362,21 @@ module QA
module Factory
module Resource
class Shirt < Factory::Base
attr_accessor :name, :size
# ... same as before
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-create-a-shirt'
end
# Attribute fetched from the API response 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,
# 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
# Attribute from the Shirt factory if present,
# or fetched from the API response if present,
# or a QA::Factory::Base::NoValueError is raised otherwise
attribute :style
def api_get_path
"/project/#{project.path}/shirt/#{name}"
# If the attribute from the Shirt factory is not present,
# and if the API does not contain this field, this block will be
# used to construct the value based on the API response.
attribute :main_fabric do
api_response.&dig(:materials, 0, 0)
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
# ... same as before
end
end
end
......@@ -411,11 +385,10 @@ end
**Notes on attributes precedence:**
- attributes from the factory have the highest precedence
- attributes from the API response take precedence over attributes from the
Browser UI
- attributes from the Browser UI take precedence over attributes from the
factory (i.e inherited)
- attributes without a value will raise a `QA::Factory::Product::NoValueError` error
block (usually from Browser UI)
- attributes without a value will raise a `QA::Factory::Base::NoValueError` error
## Creating resources in your tests
......@@ -428,42 +401,40 @@ 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'
my_shirt = Factory::Resource::Shirt.fabricate! do |shirt|
shirt.name = 'my-shirt'
end
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
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
expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block
```
If you explicitely want to use the Browser UI fabrication method, you can call
If you explicitly 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'
my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui! do |shirt|
shirt.name = 'my-shirt'
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
expect(page).to have_text(my_shirt.name) # => "my-shirt" from the factory's attribute
expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page via the block
expect(page).to have_text(my_shirt.style) # => QA::Factory::Base::NoValueError will be raised because no API response nor a block is provided
expect(page).to have_text(my_shirt.main_fabric) # => QA::Factory::Base::NoValueError will be raised because no API response and the block didn't provide a value (because it's also based on the API response)
```
You can also explicitely use the API fabrication method, by calling the
You can also explicitly 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'
my_shirt = Factory::Resource::Shirt.fabricate_via_api! do |shirt|
shirt.name = 'my-shirt'
end
```
In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`.
In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!`.
## Where to ask for help?
......
......@@ -10,13 +10,42 @@ module QA
include ApiFabricator
extend Capybara::DSL
def_delegators :evaluator, :dependency, :dependencies
def_delegators :evaluator, :product, :attributes
NoValueError = Class.new(RuntimeError)
def_delegators :evaluator, :attribute
def fabricate!(*_args)
raise NotImplementedError
end
def visit!
visit(web_url)
end
private
def populate_attribute(name, block)
value = attribute_value(name, block)
raise NoValueError, "No value was computed for product #{name} of factory #{self.class.name}." unless value
value
end
def attribute_value(name, block)
api_value = api_resource&.dig(name)
if api_value && block
log_having_both_api_result_and_block(name, api_value)
end
api_value || (block && instance_exec(&block))
end
def log_having_both_api_result_and_block(name, api_value)
QA::Runtime::Logger.info "<#{self.class}> Attribute #{name.inspect} has both API response `#{api_value}` and a block. API response will be picked. Block will be ignored."
end
def self.fabricate!(*args, &prepare_block)
fabricate_via_api!(*args, &prepare_block)
rescue NotImplementedError
......@@ -52,13 +81,10 @@ module QA
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.web_url = resource_web_url
Factory::Product.populate!(factory, resource_web_url)
Factory::Product.new(factory)
end
private_class_method :do_fabricate!
......@@ -85,31 +111,40 @@ module QA
end
private_class_method :evaluator
class DSL
attr_reader :dependencies, :attributes
def self.dynamic_attributes
const_get(:DynamicAttributes)
rescue NameError
mod = const_set(:DynamicAttributes, Module.new)
include mod
mod
end
def self.attributes_names
dynamic_attributes.instance_methods(false).sort.grep_v(/=$/)
end
class DSL
def initialize(base)
@base = base
@dependencies = []
@attributes = []
end
def dependency(factory, as:, &block)
as.tap do |name|
@base.class_eval { attr_accessor name }
def attribute(name, &block)
@base.dynamic_attributes.module_eval do
attr_writer(name)
Dependency::Signature.new(name, factory, block).tap do |signature|
@dependencies << signature
define_method(name) do
instance_variable_get("@#{name}") ||
instance_variable_set(
"@#{name}",
populate_attribute(name, block))
end
end
end
def product(attribute, &block)
Product::Attribute.new(attribute, block).tap do |signature|
@attributes << signature
end
end
end
attribute :web_url
end
end
end
module QA
module Factory
class Dependency
Signature = Struct.new(:name, :factory, :block)
def initialize(caller_factory, dependency_signature)
@caller_factory = caller_factory
@dependency_signature = dependency_signature
end
def overridden?
!!@caller_factory.public_send(@dependency_signature.name)
end
def build!(parents: [])
return if overridden?
dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory|
@dependency_signature.block&.call(factory, @caller_factory)
end
dependency.tap do |dependency|
@caller_factory.public_send("#{@dependency_signature.name}=", dependency)
end
end
end
end
end
......@@ -5,46 +5,31 @@ module QA
class Product
include Capybara::DSL
NoValueError = Class.new(RuntimeError)
attr_reader :factory
attr_reader :factory, :web_url
Attribute = Struct.new(:name, :block)
def initialize(factory, web_url)
def initialize(factory)
@factory = factory
@web_url = web_url
populate_attributes!
define_attributes
end
def visit!
visit(web_url)
end
def self.populate!(factory, web_url)
new(factory, web_url)
def populate(*attributes)
attributes.each(&method(:public_send))
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 }
def define_attributes
factory.class.attributes_names.each do |name|
define_singleton_method(name) do
factory.public_send(name)
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
......@@ -2,13 +2,14 @@ module QA
module Factory
module Repository
class ProjectPush < Factory::Repository::Push
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-code'
project.description = 'Project with repository'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-code'
resource.description = 'Project with repository'
end
end
product :output
product :project
attribute :output
def initialize
@file_name = 'file.txt'
......
......@@ -2,10 +2,12 @@ module QA
module Factory
module Repository
class WikiPush < Factory::Repository::Push
dependency Factory::Resource::Wiki, as: :wiki do |wiki|
wiki.title = 'Home'
wiki.content = '# My First Wiki Content'
wiki.message = 'Update home'
attribute :wiki do
Factory::Resource::Wiki.fabricate! do |resource|
resource.title = 'Home'
resource.content = '# My First Wiki Content'
resource.message = 'Update home'
end
end
def initialize
......
......@@ -5,8 +5,10 @@ module QA
attr_accessor :project, :branch_name,
:allow_to_push, :allow_to_merge, :protected
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'protected-branch-project'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'protected-branch-project'
end
end
def initialize
......@@ -43,9 +45,7 @@ module QA
# to `allow_to_push` variable.
return branch unless @protected
Page::Project::Menu.act do
click_repository_settings
end
Page::Project::Menu.perform(&:click_repository_settings)
Page::Project::Settings::Repository.perform do |setting|
setting.expand_protected_branches do |page|
......
......@@ -4,11 +4,11 @@ module QA
class DeployKey < Factory::Base
attr_accessor :title, :key
product :fingerprint do |resource|
Page::Project::Settings::Repository.act do
expand_deploy_keys do |key|
key_offset = key.key_titles.index do |title|
title.text == resource.title
attribute :fingerprint do
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |key|
key_offset = key.key_titles.index do |key_title|
key_title.text == title
end
key.key_fingerprints[key_offset].text
......@@ -16,17 +16,17 @@ module QA
end
end
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-deploy'
project.description = 'project for adding deploy key test'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-deploy'
resource.description = 'project for adding deploy key test'
end
end
def fabricate!
project.visit!
Page::Project::Menu.act do
click_repository_settings
end
Page::Project::Menu.perform(&:click_repository_settings)
Page::Project::Settings::Repository.perform do |setting|
setting.expand_deploy_keys do |page|
......
......@@ -4,25 +4,27 @@ module QA
class DeployToken < Factory::Base
attr_accessor :name, :expires_at
product :username do |resource|
Page::Project::Settings::Repository.act do
expand_deploy_tokens do |token|
attribute :username do
Page::Project::Settings::Repository.perform do |page|
page.expand_deploy_tokens do |token|
token.token_username
end
end
end
product :password do |password|
Page::Project::Settings::Repository.act do
expand_deploy_tokens do |token|
attribute :password do
Page::Project::Settings::Repository.perform do |page|
page.expand_deploy_tokens do |token|
token.token_password
end
end
end
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-to-deploy'
project.description = 'project for adding deploy token test'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-to-deploy'
resource.description = 'project for adding deploy token test'
end
end
def fabricate!
......
......@@ -8,8 +8,10 @@ module QA
:content,
:commit_message
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-new-file'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-new-file'
end
end
def initialize
......@@ -21,7 +23,7 @@ module QA
def fabricate!
project.visit!
Page::Project::Show.act { create_new_file! }
Page::Project::Show.perform(&:create_new_file!)
Page::File::Form.perform do |page|
page.add_name(@name)
......
......@@ -2,17 +2,19 @@ module QA
module Factory
module Resource
class Fork < Factory::Base
dependency Factory::Repository::ProjectPush, as: :push
attribute :push do
Factory::Repository::ProjectPush.fabricate!
end
dependency Factory::Resource::User, as: :user do |user|
if Runtime::Env.forker?
user.username = Runtime::Env.forker_username
user.password = Runtime::Env.forker_password
attribute :user do
Factory::Resource::User.fabricate! do |resource|
if Runtime::Env.forker?
resource.username = Runtime::Env.forker_username
resource.password = Runtime::Env.forker_password
end
end
end
product :user
def visit_project_with_retry
# The user intermittently fails to stay signed in after visiting the
# project page. The new user is registered and then signs in and a
......@@ -48,15 +50,20 @@ module QA
end
def fabricate!
push
user
visit_project_with_retry
Page::Project::Show.act { fork_project }
Page::Project::Show.perform(&:fork_project)
Page::Project::Fork::New.perform do |fork_new|
fork_new.choose_namespace(user.name)
end
Page::Layout::Banner.act { has_notice?('The project was successfully forked.') }
Page::Layout::Banner.perform do |page|
page.has_notice?('The project was successfully forked.')
end
end
end
end
......
......@@ -4,12 +4,12 @@ module QA
class Group < Factory::Base
attr_accessor :path, :description
dependency Factory::Resource::Sandbox, as: :sandbox
product :id do
true # We don't retrieve the Group ID when using the Browser UI
attribute :sandbox do
Factory::Resource::Sandbox.fabricate!
end
attribute :id
def initialize
@path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}"
......
......@@ -2,22 +2,21 @@ module QA
module Factory
module Resource
class Issue < Factory::Base
attr_accessor :title, :description, :project
attr_writer :description
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-issues'
project.description = 'project for adding issues'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-for-issues'
resource.description = 'project for adding issues'
end
end
product :project
product :title
attribute :title
def fabricate!
project.visit!
Page::Project::Show.act do
go_to_new_issue
end
Page::Project::Show.perform(&:go_to_new_issue)
Page::Project::Issue::New.perform do |page|
page.add_title(@title)
......
......@@ -7,24 +7,21 @@ module QA
attr_writer :project, :cluster,
:install_helm_tiller, :install_ingress, :install_prometheus, :install_runner
product :ingress_ip do
Page::Project::Operations::Kubernetes::Show.perform do |page|
page.ingress_ip
end
attribute :ingress_ip do
Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip)
end
def fabricate!
@project.visit!
Page::Project::Menu.act { click_operations_kubernetes }
Page::Project::Menu.perform(
&:click_operations_kubernetes)
Page::Project::Operations::Kubernetes::Index.perform do |page|
page.add_kubernetes_cluster
end
Page::Project::Operations::Kubernetes::Index.perform(
&:add_kubernetes_cluster)
Page::Project::Operations::Kubernetes::Add.perform do |page|
page.add_existing_cluster
end
Page::Project::Operations::Kubernetes::Add.perform(
&:add_existing_cluster)
Page::Project::Operations::Kubernetes::AddExisting.perform do |page|
page.set_cluster_name(@cluster.cluster_name)
......
......@@ -4,14 +4,14 @@ module QA
module Factory
module Resource
class Label < Factory::Base
attr_accessor :title,
:description,
:color
attr_accessor :description, :color
product(:title) { |factory| factory.title }
attribute :title
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-label'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-label'
end
end
def initialize
......@@ -23,8 +23,8 @@ module QA
def fabricate!
project.visit!
Page::Project::Menu.act { go_to_labels }
Page::Label::Index.act { go_to_new_label }
Page::Project::Menu.perform(&:go_to_labels)
Page::Label::Index.perform(&:go_to_new_label)
Page::Label::New.perform do |page|
page.fill_title(@title)
......
......@@ -12,27 +12,33 @@ module QA
:milestone,
:labels
product :project
product :source_branch
attribute :source_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-merge-request'
end
end
dependency Factory::Repository::ProjectPush, as: :target do |push, factory|
factory.project.visit!
push.project = factory.project
push.branch_name = 'master'
push.remote_branch = factory.target_branch
attribute :target do
project.visit!
Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.branch_name = 'master'
resource.remote_branch = target_branch
end
end
dependency Factory::Repository::ProjectPush, as: :source do |push, factory|
push.project = factory.project
push.branch_name = factory.target_branch
push.remote_branch = factory.source_branch
push.new_branch = false
push.file_name = "added_file.txt"
push.file_content = "File Added"
attribute :source do
Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = project
resource.branch_name = target_branch
resource.remote_branch = source_branch
resource.new_branch = false
resource.file_name = "added_file.txt"
resource.file_content = "File Added"
end
end
def initialize
......@@ -46,8 +52,10 @@ module QA
end
def fabricate!
target
source
project.visit!
Page::Project::Show.act { new_merge_request }
Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform do |page|
page.fill_title(@title)
page.fill_description(@description)
......
......@@ -4,19 +4,24 @@ module QA
class MergeRequestFromFork < MergeRequest
attr_accessor :fork_branch
dependency Factory::Resource::Fork, as: :fork
attribute :fork do
Factory::Resource::Fork.fabricate!
end
dependency Factory::Repository::ProjectPush, as: :push do |push, factory|
push.project = factory.fork
push.branch_name = factory.fork_branch
push.file_name = 'file2.txt'
push.user = factory.fork.user
attribute :push do
Factory::Repository::ProjectPush.fabricate! do |resource|
resource.project = fork
resource.branch_name = fork_branch
resource.file_name = 'file2.txt'
resource.user = fork.user
end
end
def fabricate!
push
fork.visit!
Page::Project::Show.act { new_merge_request }
Page::MergeRequest::New.act { create_merge_request }
Page::Project::Show.perform(&:new_merge_request)
Page::MergeRequest::New.perform(&:create_merge_request)
end
end
end
......
......@@ -7,13 +7,13 @@ module QA
class PersonalAccessToken < Factory::Base
attr_accessor :name
product :access_token do
Page::Profile::PersonalAccessTokens.act { created_access_token }
attribute :access_token do
Page::Profile::PersonalAccessTokens.perform(&:created_access_token)
end
def fabricate!
Page::Main::Menu.act { go_to_profile_settings }
Page::Profile::Menu.act { click_access_tokens }
Page::Main::Menu.perform(&:go_to_profile_settings)
Page::Profile::Menu.perform(&:click_access_tokens)
Page::Profile::PersonalAccessTokens.perform do |page|
page.fill_token_name(name || 'api-test-token')
......
......@@ -4,25 +4,24 @@ module QA
module Factory
module Resource
class Project < Factory::Base
attr_accessor :description
attr_reader :name
attribute :name
attribute :description
dependency Factory::Resource::Group, as: :group
product :group
product :name
attribute :group do
Factory::Resource::Group.fabricate!
end
product :repository_ssh_location do
Page::Project::Show.act do
choose_repository_clone_ssh
repository_location
attribute :repository_ssh_location do
Page::Project::Show.perform do |page|
page.choose_repository_clone_ssh
page.repository_location
end
end
product :repository_http_location do
Page::Project::Show.act do
choose_repository_clone_http
repository_location
attribute :repository_http_location do
Page::Project::Show.perform do |page|
page.choose_repository_clone_http
page.repository_location
end
end
......@@ -37,7 +36,7 @@ module QA
def fabricate!
group.visit!
Page::Group::Show.act { go_to_new_project }
Page::Group::Show.perform(&:go_to_new_project)
Page::Project::New.perform do |page|
page.choose_test_namespace
......
......@@ -6,14 +6,16 @@ module QA
class ProjectImportedFromGithub < Resource::Project
attr_writer :personal_access_token, :github_repository_path
dependency Factory::Resource::Group, as: :group
attribute :group do
Factory::Resource::Group.fabricate!
end
product :name
attribute :name
def fabricate!
group.visit!
Page::Group::Show.act { go_to_new_project }
Page::Group::Show.perform(&:go_to_new_project)
Page::Project::New.perform do |page|
page.go_to_import_project
......
......@@ -3,11 +3,12 @@ module QA
module Resource
class ProjectMilestone < Factory::Base
attr_accessor :description
attr_reader :title
dependency Factory::Resource::Project, as: :project
attribute :project do
Factory::Resource::Project.fabricate!
end
product :title
attribute :title
def title=(title)
@title = "#{title}-#{SecureRandom.hex(4)}"
......@@ -17,12 +18,12 @@ module QA
def fabricate!
project.visit!
Page::Project::Menu.act do
click_issues
click_milestones
Page::Project::Menu.perform do |page|
page.click_issues
page.click_milestones
end
Page::Project::Milestone::Index.act { click_new_milestone }
Page::Project::Milestone::Index.perform(&:click_new_milestone)
Page::Project::Milestone::New.perform do |milestone_new|
milestone_new.set_title(@title)
......
......@@ -6,9 +6,11 @@ module QA
class Runner < Factory::Base
attr_writer :name, :tags, :image
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-ci-cd'
project.description = 'Project with CI/CD Pipelines'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-ci-cd'
resource.description = 'Project with CI/CD Pipelines'
end
end
def name
......@@ -26,7 +28,7 @@ module QA
def fabricate!
project.visit!
Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Menu.perform(&:click_ci_cd_settings)
Service::Runner.new(name).tap do |runner|
Page::Project::Settings::CICD.perform do |settings|
......
......@@ -8,17 +8,15 @@ module QA
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
attribute :id
attribute :path
def initialize
@path = Runtime::Namespace.sandbox_name
end
def fabricate!
Page::Main::Menu.act { go_to_groups }
Page::Main::Menu.perform(&:go_to_groups)
Page::Dashboard::Groups.perform do |page|
if page.has_group?(path)
......
......@@ -4,15 +4,17 @@ module QA
class SecretVariable < Factory::Base
attr_accessor :key, :value
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-secret-variables'
project.description = 'project for adding secret variable test'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-with-secret-variables'
resource.description = 'project for adding secret variable test'
end
end
def fabricate!
project.visit!
Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Menu.perform(&:click_ci_cd_settings)
Page::Project::Settings::CICD.perform do |setting|
setting.expand_secret_variables do |page|
......
......@@ -6,21 +6,19 @@ module QA
class SSHKey < Factory::Base
extend Forwardable
attr_accessor :title
attr_reader :private_key, :public_key, :fingerprint
def_delegators :key, :private_key, :public_key, :fingerprint
product :private_key
product :title
product :fingerprint
attribute :private_key
attribute :title
attribute :fingerprint
def key
@key ||= Runtime::Key::RSA.new
end
def fabricate!
Page::Main::Menu.act { go_to_profile_settings }
Page::Profile::Menu.act { click_ssh_keys }
Page::Main::Menu.perform(&:go_to_profile_settings)
Page::Profile::Menu.perform(&:click_ssh_keys)
Page::Profile::SSHKeys.perform do |page|
page.add_key(public_key, title)
......
......@@ -5,7 +5,6 @@ module QA
module Resource
class User < Factory::Base
attr_reader :unique_id
attr_writer :username, :password, :name, :email
def initialize
@unique_id = SecureRandom.hex(8)
......@@ -31,14 +30,14 @@ module QA
defined?(@username) && defined?(@password)
end
product :name
product :username
product :email
product :password
attribute :name
attribute :username
attribute :email
attribute :password
def fabricate!
# Don't try to log-out if we're not logged-in
if Page::Main::Menu.act { has_personal_area?(wait: 0) }
if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) }
Page::Main::Menu.perform { |main| main.sign_out }
end
......
......@@ -4,9 +4,11 @@ module QA
class Wiki < Factory::Base
attr_accessor :title, :content, :message
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-for-wikis'
project.description = 'project for adding wikis'
attribute :project do
Factory::Resource::Project.fabricate! do |resource|
resource.name = 'project-for-wikis'
resource.description = 'project for adding wikis'
end
end
def fabricate!
......
......@@ -5,9 +5,9 @@ module QA
def fabricate!(*traits)
raise ArgumentError unless traits.include?(:enabled)
Page::Main::Login.act { sign_in_using_credentials }
Page::Main::Menu.act { go_to_admin_area }
Page::Admin::Menu.act { go_to_repository_settings }
Page::Main::Login.perform(&:sign_in_using_credentials)
Page::Main::Menu.perform(&:go_to_admin_area)
Page::Admin::Menu.perform(&:go_to_repository_settings)
Page::Admin::Settings::Repository.perform do |setting|
setting.expand_repository_storage do |page|
......@@ -16,7 +16,7 @@ module QA
end
end
QA::Page::Main::Menu.act { sign_out }
QA::Page::Main::Menu.perform(&:sign_out)
end
end
end
......
......@@ -7,7 +7,7 @@ module QA
module Logger
extend SingleForwardable
def_delegators :logger, :debug, :info, :error, :warn, :fatal, :unknown
def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown
singleton_class.module_eval do
def logger
......
......@@ -49,11 +49,13 @@ module QA
cluster.install_prometheus = true
cluster.install_runner = true
end
kubernetes_cluster.populate(:ingress_ip)
project.visit!
Page::Project::Menu.act { click_ci_cd_settings }
Page::Project::Settings::CICD.perform do |p|
p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io")
p.enable_auto_devops_with_domain(
"#{kubernetes_cluster.ingress_ip}.nip.io")
end
project.visit!
......
......@@ -19,7 +19,7 @@ describe QA::Factory::Base do
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)
allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product)
end
end
......@@ -115,73 +115,134 @@ describe QA::Factory::Base do
end
end
describe '.dependency' do
let(:dependency) { spy('dependency') }
shared_context 'simple factory' do
subject do
Class.new(QA::Factory::Base) do
attribute :test do
'block'
end
before do
stub_const('Some::MyDependency', dependency)
end
attribute :no_block
subject do
Class.new(described_class) do
dependency Some::MyDependency, as: :mydep do |factory|
factory.something!
def fabricate!
'any'
end
def self.current_url
'http://stub'
end
end
end
it 'appends a new dependency and accessors' do
expect(subject.dependencies).to be_one
let(:factory) { subject.new }
end
describe '.attribute' do
include_context 'simple factory'
it 'appends new product attribute' do
expect(subject.attributes_names).to eq([:no_block, :test, :web_url])
end
it 'defines dependency accessors' do
expect(subject.new).to respond_to :mydep, :mydep=
context 'when the product attribute is populated via a block' do
it 'returns a fabrication product and defines factory attributes as its methods' do
result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product)
expect(result.test).to eq('block')
end
end
describe 'dependencies fabrication' do
let(:dependency) { double('dependency') }
let(:instance) { spy('instance') }
context 'when the product attribute is populated via the api' do
let(:api_resource) { { no_block: 'api' } }
before do
expect(factory).to receive(:api_resource).and_return(api_resource)
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product)
expect(result.no_block).to eq('api')
end
context 'when the attribute also has a block in the factory' do
let(:api_resource) { { test: 'api_with_block' } }
before do
allow(QA::Runtime::Logger).to receive(:info)
end
it 'returns the api value and emits an INFO log entry' do
result = subject.fabricate!(factory: factory)
subject do
Class.new(described_class) do
dependency Some::MyDependency, as: :mydep
expect(result).to be_a(QA::Factory::Product)
expect(result.test).to eq('api_with_block')
expect(QA::Runtime::Logger)
.to have_received(:info).with(/api_with_block/)
end
end
end
context 'when the product attribute is populated via a factory attribute' do
before do
stub_const('Some::MyDependency', dependency)
factory.test = 'value'
end
it 'returns a fabrication product and defines factory attributes as its methods' do
result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product)
expect(result.test).to eq('value')
end
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)
expect(QA::Factory::Product).to receive(:populate!)
context 'when the api also has such response' do
before do
allow(factory).to receive(:api_resource).and_return({ test: 'api' })
end
it 'returns the factory attribute for the product' do
result = subject.fabricate!(factory: factory)
expect(result).to be_a(QA::Factory::Product)
expect(result.test).to eq('value')
end
end
end
it 'builds all dependencies first' do
expect(dependency).to receive(:fabricate!).once
context 'when the product attribute has no value' do
it 'raises an error because no values could be found' do
result = subject.fabricate!(factory: factory)
subject.fabricate!
expect { result.no_block }
.to raise_error(described_class::NoValueError, "No value was computed for product no_block of factory #{factory.class.name}.")
end
end
end
describe '.product' do
include_context 'fabrication context'
describe '#web_url' do
include_context 'simple factory'
subject do
Class.new(described_class) do
def fabricate!
"any"
end
it 'sets #web_url to #current_url after fabrication' do
subject.fabricate!(factory: factory)
product :token
end
expect(factory.web_url).to eq(subject.current_url)
end
end
describe '#visit!' do
include_context 'simple factory'
before do
allow(factory).to receive(:visit)
end
it 'appends new product attribute' do
expect(subject.attributes).to be_one
expect(subject.attributes[0]).to be_a(QA::Factory::Product::Attribute)
expect(subject.attributes[0].name).to eq(:token)
it 'calls #visit with the underlying #web_url' do
factory.web_url = subject.current_url
factory.visit!
expect(factory).to have_received(:visit).with(subject.current_url)
end
end
end
describe QA::Factory::Dependency do
let(:dependency) { spy('dependency' ) }
let(:factory) { spy('factory') }
let(:block) { spy('block') }
let(:signature) do
double('signature', name: :mydep, factory: dependency, block: block)
end
subject do
described_class.new(factory, signature)
end
describe '#overridden?' do
it 'returns true if factory has overridden dependency' do
allow(factory).to receive(:mydep).and_return('something')
expect(subject).to be_overridden
end
it 'returns false if dependency has not been overridden' do
allow(factory).to receive(:mydep).and_return(nil)
expect(subject).not_to be_overridden
end
end
describe '#build!' do
context 'when dependency has been overridden' do
before do
allow(subject).to receive(:overridden?).and_return(true)
end
it 'does not fabricate dependency' do
subject.build!
expect(dependency).not_to have_received(:fabricate!)
end
end
context 'when dependency has not been overridden' do
before do
allow(subject).to receive(:overridden?).and_return(false)
end
it 'fabricates dependency' do
subject.build!
expect(dependency).to have_received(:fabricate!)
end
it 'sets product in the factory' do
subject.build!
expect(factory).to have_received(:mydep=).with(dependency)
end
it 'calls given block with dependency factory and caller factory' do
expect(dependency).to receive(:fabricate!).and_yield(dependency)
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(dependency).to have_received(:fabricate!)
end
end
end
end
end
describe QA::Factory::Product do
let(:factory) do
Class.new(QA::Factory::Base) do
def foo
'bar'
attribute :test do
'block'
end
attribute :no_block
end.new
end
let(:product) { spy('product') }
let(:product_location) { 'http://product_location' }
subject { described_class.new(factory, product_location) }
describe '.populate!' do
before do
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
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)
subject { described_class.new(factory) }
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
before do
factory.web_url = product_location
end
describe '.visit!' do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment