diff --git a/qa/qa.rb b/qa/qa.rb index 35ff7458c34..7feca22478a 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -36,6 +36,7 @@ module QA # GitLab QA fabrication mechanisms # module Factory + autoload :ApiFabricator, 'qa/factory/api_fabricator' autoload :Base, 'qa/factory/base' autoload :Dependency, 'qa/factory/dependency' autoload :Product, 'qa/factory/product' diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md new file mode 100644 index 00000000000..c56c7c43129 --- /dev/null +++ b/qa/qa/factory/README.md @@ -0,0 +1,476 @@ +# Factory objects in GitLab QA + +In GitLab QA we are using factories to create resources. + +Factories implementation are primarily done using Browser UI steps, but can also +be done via the API. + +## Why do we need that? + +We need factory objects because we need to reduce duplication when creating +resources for our QA tests. + +## How to properly implement a factory object? + +All factories should inherit from [`Factory::Base`](./base.rb). + +There is only one mandatory method to implement to define a factory. This is the +`#fabricate!` method, which is used to build a resource via the browser UI. +Note that you should only use [Page objects](../page/README.md) to interact with +a Web page in this method. + +Here is an imaginary example: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attr_accessor :name, :size + + def initialize(name) + @name = name + end + + def fabricate! + Page::Dashboard::Index.perform do |dashboard_index| + dashboard_index.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + end + end + end +end +``` + +### Define API implementation + +A factory may also implement the three following methods to be able to create a +resource via the public GitLab API: + +- `#api_get_path`: The `GET` path to fetch an existing resource. +- `#api_post_path`: The `POST` path to create a new resource. +- `#api_post_body`: The `POST` body (as a Ruby hash) to create a new resource. + +Let's take the `Shirt` factory example, and add these three API methods: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attr_accessor :name, :size + + def initialize(name) + @name = name + end + + def fabricate! + Page::Dashboard::Index.perform do |dashboard_index| + dashboard_index.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/shirt/#{name}" + end + + def api_post_path + "/shirts" + end + + def api_post_body + { + name: name + } + end + end + end + end +end +``` + +The [`Project` factory](./resource/project.rb) is a good real example of Browser +UI and API implementations. + +### Define dependencies + +A resource may need an other resource to exist first. For instance, a project +needs a group to be created in. + +To define a dependency, you can use the `dependency` DSL method. +The first argument is a factory class, then you should pass `as: ` to give +a name to the dependency. +That will allow access to the dependency from your resource object's methods. +You would usually use it in `#fabricate!`, `#api_get_path`, `#api_post_path`, +`#api_post_body`. + +Let's take the `Shirt` factory, and add a `project` dependency to it: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attr_accessor :name, :size + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-to-create-a-shirt' + end + + def initialize(name) + @name = name + end + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/project/#{project.path}/shirt/#{name}" + end + + def api_post_path + "/project/#{project.path}/shirts" + end + + def api_post_body + { + name: name + } + end + end + end + end +end +``` + +**Note that dependencies are always built via the API fabrication method if +supported by their factories.** + +### Define attributes on the created resource + +Once created, you may want to populate a resource with attributes that can be +found in the Web page, or in the API response. +For instance, once you create a project, you may want to store its repository +SSH URL as an attribute. + +To define an attribute, you can use the `product` DSL method. +The first argument is the attribute name, then you should define a name for the +dependency to be accessible from your resource object's methods. + +Let's take the `Shirt` factory, and define a `:brand` attribute: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attr_accessor :name, :size + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-to-create-a-shirt' + end + + # Attribute populated from the Browser UI (using the block) + product :brand do + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + end + + def initialize(name) + @name = name + end + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/project/#{project.path}/shirt/#{name}" + end + + def api_post_path + "/project/#{project.path}/shirts" + end + + def api_post_body + { + name: name + } + end + end + end + end +end +``` + +#### Inherit a factory's attribute + +Sometimes, you want a resource to inherit its factory attributes. For instance, +it could be useful to pass the `size` attribute from the `Shirt` factory to the +created resource. +You can do that by defining `product :attribute_name` without a block. + +Let's take the `Shirt` factory, and define a `:name` and a `:size` attributes: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attr_accessor :name, :size + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-to-create-a-shirt' + end + + # Attribute inherited from the Shirt factory if present, + # or from the Browser UI otherwise (using the block) + product :brand do + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + end + + # Attribute inherited from the Shirt factory if present, + # or a QA::Factory::Product::NoValueError is raised otherwise + product :name + product :size + + def initialize(name) + @name = name + end + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/project/#{project.path}/shirt/#{name}" + end + + def api_post_path + "/project/#{project.path}/shirts" + end + + def api_post_body + { + name: name + } + end + end + end + end +end +``` + +#### Define an attribute based on an API response + +Sometimes, you want to define a resource attribute based on the API response +from its `GET` or `POST` request. For instance, if the creation of a shirt via +the API returns + +```ruby +{ + brand: 'a-brand-new-brand', + size: 'extra-small', + style: 't-shirt', + materials: [[:cotton, 80], [:polyamide, 20]] +} +``` + +you may want to store `style` as-is in the resource, and fetch the first value +of the first `materials` item in a `main_fabric` attribute. + +For both attributes, you will need to define an inherited attribute, as shown +in "Inherit a factory's attribute" above, but in the case of `main_fabric`, you +will need to implement the +`#transform_api_resource` method to first populate the `:main_fabric` key in the +API response so that it can be used later to automatically populate the +attribute on your resource. + +If an attribute can only be retrieved from the API response, you should define +a block to give it a default value, otherwise you could get a +`QA::Factory::Product::NoValueError` when creating your resource via the +Browser UI. + +Let's take the `Shirt` factory, and define a `:style` and a `:main_fabric` +attributes: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attr_accessor :name, :size + + dependency Factory::Resource::Project, as: :project do |project| + project.name = 'project-to-create-a-shirt' + end + + # Attribute fetched from the API response if present if present, + # or from the Shirt factory if present, + # or from the Browser UI otherwise (using the block) + product :brand do + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + end + + # Attribute fetched from the API response if present if present, + # or from the Shirt factory if present, + # or a QA::Factory::Product::NoValueError is raised otherwise + product :name + product :size + product :style do + 'unknown' + end + product :main_fabric do + 'unknown' + end + + def initialize(name) + @name = name + end + + def fabricate! + project.visit! + + Page::Project::Show.perform do |project_show| + project_show.go_to_new_shirt + end + + Page::Shirt::New.perform do |shirt_new| + shirt_new.set_name(name) + shirt_new.create_shirt! + end + end + + def api_get_path + "/project/#{project.path}/shirt/#{name}" + end + + def api_post_path + "/project/#{project.path}/shirts" + end + + def api_post_body + { + name: name + } + end + + private + + def transform_api_resource(api_response) + api_response[:main_fabric] = api_response[:materials][0][0] + api_response + end + end + end + end +end +``` + +**Notes on attributes precedence:** + +- attributes from the API response take precedence over attributes from the + factory (i.e inherited) +- attributes from the factory (i.e inherited) take precedence over attributes + from the Browser UI +- attributes without a value will raise a `QA::Factory::Product::NoValueError` error + +## Creating resources in your tests + +To create a resource in your tests, you can call the `.fabricate!` method on the +factory class. +Note that if the factory supports API fabrication, this will use this +fabrication by default. + +Here is an example that will use the API fabrication method under the hood since +it's supported by the `Shirt` factory: + +```ruby +my_shirt = Factory::Resource::Shirt.fabricate!('my-shirt') do |shirt| + shirt.size = 'small' +end + +expect(page).to have_text(my_shirt.brand) # => "a-brand-new-brand" from the API response +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute +expect(page).to have_text(my_shirt.size) # => "extra-small" from the API response +expect(page).to have_text(my_shirt.style) # => "t-shirt" from the API response +expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the (transformed) API response +``` + +If you explicitely want to use the Browser UI fabrication method, you can call +the `.fabricate_via_browser_ui!` method instead: + +```ruby +my_shirt = Factory::Resource::Shirt.fabricate_via_browser_ui!('my-shirt') do |shirt| + shirt.size = 'small' +end + +expect(page).to have_text(my_shirt.brand) # => the brand name fetched from the `Page::Shirt::Show` page +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the inherited factory's attribute +expect(page).to have_text(my_shirt.size) # => "small" from the inherited factory's attribute +expect(page).to have_text(my_shirt.style) # => "unknown" from the attribute block +expect(page).to have_text(my_shirt.main_fabric) # => "unknown" from the attribute block +``` + +You can also explicitely use the API fabrication method, by calling the +`.fabricate_via_api!` method: + +```ruby +my_shirt = Factory::Resource::Shirt.fabricate_via_api!('my-shirt') do |shirt| + shirt.size = 'small' +end +``` + +In this case, the result will be similar to calling `Factory::Resource::Shirt.fabricate!('my-shirt')`. + +## Where to ask for help? + +If you need more information, ask for help on `#quality` channel on Slack +(internal, GitLab Team only). + +If you are not a Team Member, and you still need help to contribute, please +open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/factory/api_fabricator.rb b/qa/qa/factory/api_fabricator.rb new file mode 100644 index 00000000000..b1cfb6c9783 --- /dev/null +++ b/qa/qa/factory/api_fabricator.rb @@ -0,0 +1,97 @@ +# frozen_string_literal: true + +require 'airborne' +require 'active_support/core_ext/object/deep_dup' +require 'capybara/dsl' + +module QA + module Factory + module ApiFabricator + include Airborne + include Capybara::DSL + + HTTP_STATUS_OK = 200 + HTTP_STATUS_CREATED = 201 + + ResourceNotFoundError = Class.new(RuntimeError) + ResourceFabricationFailedError = Class.new(RuntimeError) + ResourceURLMissingError = Class.new(RuntimeError) + + attr_reader :api_resource, :api_response + + def api_support? + respond_to?(:api_get_path) && + respond_to?(:api_post_path) && + respond_to?(:api_post_body) + end + + def fabricate_via_api! + unless api_support? + raise NotImplementedError, "Factory #{self.class.name} does not support fabrication via the API!" + end + + resource_web_url(api_post) + end + + def eager_load_api_client! + api_client.tap do |client| + # Eager-load the API client so that the personal token creation isn't + # taken in account in the actual resource creation timing. + client.personal_access_token + end + end + + private + + attr_writer :api_resource, :api_response + + def resource_web_url(resource) + resource.fetch(:web_url) do + raise ResourceURLMissingError, "API resource for #{self.class.name} does not expose a `web_url` property: `#{resource}`." + end + end + + def api_get + url = Runtime::API::Request.new(api_client, api_get_path).url + response = get(url) + + unless response.code == HTTP_STATUS_OK + raise ResourceNotFoundError, "Resource at #{url} could not be found (#{response.code}): `#{response}`." + end + + process_api_response(parse_body(response)) + end + + def api_post + response = post( + Runtime::API::Request.new(api_client, api_post_path).url, + api_post_body) + + unless response.code == HTTP_STATUS_CREATED + raise ResourceFabricationFailedError, "Fabrication of #{self.class.name} using the API failed (#{response.code}) with `#{response}`." + end + + process_api_response(parse_body(response)) + end + + def api_client + @api_client ||= begin + Runtime::API::Client.new(:gitlab, is_new_session: !current_url.start_with?('http')) + end + end + + def parse_body(response) + JSON.parse(response.body, symbolize_names: true) + end + + def process_api_response(parsed_response) + self.api_response = parsed_response + self.api_resource = transform_api_resource(parsed_response.deep_dup) + end + + def transform_api_resource(resource) + resource + end + end + end +end diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb index 7a532ce534b..a8ecac2a1e6 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/factory/base.rb @@ -1,9 +1,14 @@ +# frozen_string_literal: true + require 'forwardable' +require 'capybara/dsl' module QA module Factory class Base extend SingleForwardable + include ApiFabricator + extend Capybara::DSL def_delegators :evaluator, :dependency, :dependencies def_delegators :evaluator, :product, :attributes @@ -12,46 +17,96 @@ module QA raise NotImplementedError end - def self.fabricate!(*args) - new.tap do |factory| - yield factory if block_given? + def self.fabricate!(*args, &prepare_block) + fabricate_via_api!(*args, &prepare_block) + rescue NotImplementedError + fabricate_via_browser_ui!(*args, &prepare_block) + end - dependencies.each do |name, signature| - Factory::Dependency.new(name, factory, signature).build! - end + def self.fabricate_via_browser_ui!(*args, &prepare_block) + options = args.extract_options! + factory = options.fetch(:factory) { new } + parents = options.fetch(:parents) { [] } - 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 diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb index fc5dc82ce29..655e2677db0 100644 --- a/qa/qa/factory/dependency.rb +++ b/qa/qa/factory/dependency.rb @@ -1,37 +1,26 @@ module QA module Factory class Dependency - Signature = Struct.new(:factory, :block) + Signature = Struct.new(:name, :factory, :block) - def initialize(name, factory, signature) - @name = name - @factory = factory - @signature = signature + def initialize(caller_factory, dependency_signature) + @caller_factory = caller_factory + @dependency_signature = dependency_signature end def overridden? - !!@factory.public_send(@name) + !!@caller_factory.public_send(@dependency_signature.name) end - def build! + def build!(parents: []) return if overridden? - Builder.new(@signature, @factory).fabricate!.tap do |product| - @factory.public_send("#{@name}=", product) - end - end - - class Builder - def initialize(signature, caller_factory) - @factory = signature.factory - @block = signature.block - @caller_factory = caller_factory + dependency = @dependency_signature.factory.fabricate!(parents: parents) do |factory| + @dependency_signature.block&.call(factory, @caller_factory) end - def fabricate! - @factory.fabricate! do |factory| - @block&.call(factory, @caller_factory) - end + dependency.tap do |dependency| + @caller_factory.public_send("#{@dependency_signature.name}=", dependency) end end end diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb index 996b7f14f61..17fe908eaa2 100644 --- a/qa/qa/factory/product.rb +++ b/qa/qa/factory/product.rb @@ -5,26 +5,46 @@ module QA class Product include Capybara::DSL + NoValueError = Class.new(RuntimeError) + + attr_reader :factory, :web_url + Attribute = Struct.new(:name, :block) - def initialize - @location = current_url + def initialize(factory, web_url) + @factory = factory + @web_url = web_url + + populate_attributes! end def visit! - visit @location + visit(web_url) end - def self.populate!(factory) - 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 diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb index 167f47c9141..6f878396f0e 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/factory/repository/project_push.rb @@ -7,13 +7,8 @@ module QA project.description = 'Project with repository' end - product :output do |factory| - factory.output - end - - product :project do |factory| - factory.project - end + product :output + product :project def initialize @file_name = 'file.txt' diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb index 83dd4000f0a..6e2a668df64 100644 --- a/qa/qa/factory/resource/fork.rb +++ b/qa/qa/factory/resource/fork.rb @@ -11,7 +11,7 @@ module QA end end - product(:user) { |factory| factory.user } + product :user def visit_project_with_retry # The user intermittently fails to stay signed in after visiting the diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb index 033fc48c08f..2688328df92 100644 --- a/qa/qa/factory/resource/group.rb +++ b/qa/qa/factory/resource/group.rb @@ -6,6 +6,10 @@ module QA dependency Factory::Resource::Sandbox, as: :sandbox + product :id do + true # We don't retrieve the Group ID when using the Browser UI + end + def initialize @path = Runtime::Namespace.name @description = "QA test run at #{Runtime::Namespace.time}" @@ -35,6 +39,29 @@ module QA end end end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{CGI.escape("#{sandbox.path}/#{path}")}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + parent_id: sandbox.id, + path: path, + name: path, + visibility: 'public' + } + end end end end diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb index 95f48e20b3e..9b444cb0bf1 100644 --- a/qa/qa/factory/resource/issue.rb +++ b/qa/qa/factory/resource/issue.rb @@ -2,16 +2,15 @@ module QA module Factory module Resource class Issue < Factory::Base - attr_writer :title, :description, :project + attr_accessor :title, :description, :project dependency Factory::Resource::Project, as: :project do |project| project.name = 'project-for-issues' project.description = 'project for adding issues' end - product :title do - Page::Project::Issue::Show.act { issue_title } - end + product :project + product :title def fabricate! project.visit! diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb index c4620348202..18046c7a8b2 100644 --- a/qa/qa/factory/resource/merge_request.rb +++ b/qa/qa/factory/resource/merge_request.rb @@ -12,13 +12,8 @@ module QA :milestone, :labels - product :project do |factory| - factory.project - end - - product :source_branch do |factory| - factory.source_branch - end + product :project + product :source_branch dependency Factory::Resource::Project, as: :project do |project| project.name = 'project-with-merge-request' diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb index 90db26ab3ab..105e42b23ec 100644 --- a/qa/qa/factory/resource/project.rb +++ b/qa/qa/factory/resource/project.rb @@ -4,14 +4,13 @@ module QA module Factory module Resource class Project < Factory::Base - attr_writer :description + attr_accessor :description attr_reader :name dependency Factory::Resource::Group, as: :group - product :name do |factory| - factory.name - end + product :group + product :name product :repository_ssh_location do Page::Project::Show.act do @@ -48,6 +47,32 @@ module QA page.create_new_project end end + + def api_get_path + "/projects/#{name}" + end + + def api_post_path + '/projects' + end + + def api_post_body + { + namespace_id: group.id, + path: name, + name: name, + description: description, + visibility: 'public' + } + end + + private + + def transform_api_resource(resource) + resource[:repository_ssh_location] = Git::Location.new(resource[:ssh_url_to_repo]) + resource[:repository_http_location] = Git::Location.new(resource[:http_url_to_repo]) + resource + end end end end diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb index df2a3340d60..a45e7fee03b 100644 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ b/qa/qa/factory/resource/project_imported_from_github.rb @@ -8,9 +8,7 @@ module QA dependency Factory::Resource::Group, as: :group - product :name do |factory| - factory.name - end + product :name def fabricate! group.visit! diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb index 1251ae03135..35383842142 100644 --- a/qa/qa/factory/resource/project_milestone.rb +++ b/qa/qa/factory/resource/project_milestone.rb @@ -7,7 +7,7 @@ module QA dependency Factory::Resource::Project, as: :project - product(:title) { |factory| factory.title } + product :title def title=(title) @title = "#{title}-#{SecureRandom.hex(4)}" diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb index 5249e1755a6..e592f4e0dd2 100644 --- a/qa/qa/factory/resource/sandbox.rb +++ b/qa/qa/factory/resource/sandbox.rb @@ -6,21 +6,28 @@ module QA # creating it if it doesn't yet exist. # class Sandbox < Factory::Base + attr_reader :path + + product :id do + true # We don't retrieve the Group ID when using the Browser UI + end + product :path + def initialize - @name = Runtime::Namespace.sandbox_name + @path = Runtime::Namespace.sandbox_name end def fabricate! Page::Main::Menu.act { go_to_groups } Page::Dashboard::Groups.perform do |page| - if page.has_group?(@name) - page.go_to_group(@name) + if page.has_group?(path) + page.go_to_group(path) else page.go_to_new_group Page::Group::New.perform do |group| - group.set_path(@name) + group.set_path(path) group.set_description('GitLab QA Sandbox Group') group.set_visibility('Public') group.create @@ -28,6 +35,28 @@ module QA end end end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/groups/#{path}" + end + + def api_post_path + '/groups' + end + + def api_post_body + { + path: path, + name: path, + visibility: 'public' + } + end end end end diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb index 45236f69de9..a512d071dd4 100644 --- a/qa/qa/factory/resource/ssh_key.rb +++ b/qa/qa/factory/resource/ssh_key.rb @@ -10,17 +10,9 @@ module QA attr_reader :private_key, :public_key, :fingerprint def_delegators :key, :private_key, :public_key, :fingerprint - product :private_key do |factory| - factory.private_key - end - - product :title do |factory| - factory.title - end - - product :fingerprint do |factory| - factory.fingerprint - end + product :private_key + product :title + product :fingerprint def key @key ||= Runtime::Key::RSA.new diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb index e8b9ea2e6b4..36edf787b64 100644 --- a/qa/qa/factory/resource/user.rb +++ b/qa/qa/factory/resource/user.rb @@ -31,10 +31,10 @@ module QA defined?(@username) && defined?(@password) end - product(:name) { |factory| factory.name } - product(:username) { |factory| factory.username } - product(:email) { |factory| factory.email } - product(:password) { |factory| factory.password } + product :name + product :username + product :email + product :password def fabricate! # Don't try to log-out if we're not logged-in diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb index acfe143fa61..d697433736e 100644 --- a/qa/qa/factory/resource/wiki.rb +++ b/qa/qa/factory/resource/wiki.rb @@ -10,13 +10,16 @@ module QA end def fabricate! - Page::Project::Menu.act { click_wiki } - Page::Project::Wiki::New.perform do |page| - page.go_to_create_first_page - page.set_title(@title) - page.set_content(@content) - page.set_message(@message) - page.create_new_page + project.visit! + + Page::Project::Menu.perform { |menu_side| menu_side.click_wiki } + + Page::Project::Wiki::New.perform do |wiki_new| + wiki_new.go_to_create_first_page + wiki_new.set_title(@title) + wiki_new.set_content(@content) + wiki_new.set_message(@message) + wiki_new.create_new_page end end end diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md index 4d58f1a43b7..d0de33892c4 100644 --- a/qa/qa/page/README.md +++ b/qa/qa/page/README.md @@ -131,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack (internal, GitLab Team only). If you are not a Team Member, and you still need help to contribute, please -open an issue in GitLab QA issue tracker. +open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index 02015e23ad8..0545b500e4c 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -6,33 +6,34 @@ module QA class Client attr_reader :address - def initialize(address = :gitlab, personal_access_token: nil) + def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true) @address = address @personal_access_token = personal_access_token + @is_new_session = is_new_session end def personal_access_token - @personal_access_token ||= get_personal_access_token - end - - def get_personal_access_token - # you can set the environment variable PERSONAL_ACCESS_TOKEN - # to use a specific access token rather than create one from the UI - if Runtime::Env.personal_access_token - Runtime::Env.personal_access_token - else - create_personal_access_token + @personal_access_token ||= begin + # you can set the environment variable PERSONAL_ACCESS_TOKEN + # to use a specific access token rather than create one from the UI + Runtime::Env.personal_access_token ||= create_personal_access_token end end private def create_personal_access_token - Runtime::Browser.visit(@address, Page::Main::Login) do - Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::PersonalAccessToken.fabricate!.access_token + if @is_new_session + Runtime::Browser.visit(@address, Page::Main::Login) { do_create_personal_access_token } + else + do_create_personal_access_token end end + + def do_create_personal_access_token + Page::Main::Login.act { sign_in_using_credentials } + Factory::Resource::PersonalAccessToken.fabricate!.access_token + end end end end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 4a2109799fa..533ed87453a 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -3,6 +3,12 @@ module QA module Env extend self + attr_writer :personal_access_token + + def verbose? + enabled?(ENV['VERBOSE'], default: false) + end + # set to 'false' to have Chrome run visibly instead of headless def chrome_headless? enabled?(ENV['CHROME_HEADLESS']) @@ -22,7 +28,7 @@ module QA # specifies token that can be used for the api def personal_access_token - ENV['PERSONAL_ACCESS_TOKEN'] + @personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN'] end def user_username @@ -42,7 +48,7 @@ module QA end def forker? - forker_username && forker_password + !!(forker_username && forker_password) end def forker_username diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index b276c7ee579..53865b44684 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -11,9 +11,10 @@ module QA Page::Main::Menu.perform { |main| main.sign_out } Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Project.fabricate! do |resource| + project = Factory::Resource::Project.fabricate! do |resource| resource.name = 'add-member-project' end + project.visit! Page::Project::Menu.act { click_members_settings } Page::Project::Settings::Members.perform do |page| diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index bb1f3ab26d1..c8ea558aed6 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -7,17 +7,15 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - created_project = Factory::Resource::Project.fabricate! do |project| + created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' end - expect(created_project.name).to match /^awesome-project-\h{16}$/ - + expect(page).to have_content(created_project.name) expect(page).to have_content( /Project \S?awesome-project\S+ was successfully created/ ) - expect(page).to have_content('create awesome project test') expect(page).to have_content('The repository for this project is empty') end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index 984cea8ca10..827dbb67076 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb @@ -10,6 +10,7 @@ module QA project = Factory::Resource::Project.fabricate! do |project| project.name = "only-fast-forward" end + project.visit! Page::Project::Menu.act { go_to_settings } Page::Project::Settings::MergeRequest.act { enable_ff_only } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index 0dcdc6639d1..a982a4604ac 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -14,10 +14,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Project.fabricate! do |scenario| + project = Factory::Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end + project.visit! Git::Repository.perform do |repository| repository.uri = location.uri diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index ab5d97d5b66..1f07d08e664 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -17,6 +17,7 @@ module QA project.name = 'file-template-project' project.description = 'Add file templates via the Web IDE' end + @project.visit! # Add a file via the regular Files view because the Web IDE isn't # available unless there is a file present diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/factory/api_fabricator_spec.rb new file mode 100644 index 00000000000..e5fbc064911 --- /dev/null +++ b/qa/spec/factory/api_fabricator_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +describe QA::Factory::ApiFabricator do + let(:factory_without_api_support) do + Class.new do + def self.name + 'FooBarFactory' + end + end + end + + let(:factory_with_api_support) do + Class.new do + def self.name + 'FooBarFactory' + end + + def api_get_path + '/foo' + end + + def api_post_path + '/bar' + end + + def api_post_body + { name: 'John Doe' } + end + end + end + + before do + allow(subject).to receive(:current_url).and_return('') + end + + subject { factory.tap { |f| f.include(described_class) }.new } + + describe '#api_support?' do + let(:api_client) { spy('Runtime::API::Client') } + let(:api_client_instance) { double('API Client') } + + context 'when factory does not support fabrication via the API' do + let(:factory) { factory_without_api_support } + + it 'returns false' do + expect(subject).not_to be_api_support + end + end + + context 'when factory supports fabrication via the API' do + let(:factory) { factory_with_api_support } + + it 'returns false' do + expect(subject).to be_api_support + end + end + end + + describe '#fabricate_via_api!' do + let(:api_client) { spy('Runtime::API::Client') } + let(:api_client_instance) { double('API Client') } + + before do + stub_const('QA::Runtime::API::Client', api_client) + + allow(api_client).to receive(:new).and_return(api_client_instance) + allow(api_client_instance).to receive(:personal_access_token).and_return('foo') + end + + context 'when factory does not support fabrication via the API' do + let(:factory) { factory_without_api_support } + + it 'raises a NotImplementedError exception' do + expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Factory FooBarFactory does not support fabrication via the API!") + end + end + + context 'when factory supports fabrication via the API' do + let(:factory) { factory_with_api_support } + let(:api_request) { spy('Runtime::API::Request') } + let(:resource_web_url) { 'http://example.org/api/v4/foo' } + let(:resource) { { id: 1, name: 'John Doe', web_url: resource_web_url } } + let(:raw_post) { double('Raw POST response', code: 201, body: resource.to_json) } + + before do + stub_const('QA::Runtime::API::Request', api_request) + + allow(api_request).to receive(:new).and_return(double(url: resource_web_url)) + end + + context 'when creating a resource' do + before do + allow(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) + end + + it 'returns the resource URL' do + expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url)) + expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) + + expect(subject.fabricate_via_api!).to eq(resource_web_url) + end + + it 'populates api_resource with the resource' do + subject.fabricate_via_api! + + expect(subject.api_resource).to eq(resource) + end + + context 'when the POST fails' do + let(:post_response) { { error: "Name already taken." } } + let(:raw_post) { double('Raw POST response', code: 400, body: post_response.to_json) } + + it 'raises a ResourceFabricationFailedError exception' do + expect(api_request).to receive(:new).with(api_client_instance, subject.api_post_path).and_return(double(url: resource_web_url)) + expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) + + expect { subject.fabricate_via_api! }.to raise_error(described_class::ResourceFabricationFailedError, "Fabrication of FooBarFactory using the API failed (400) with `#{raw_post}`.") + expect(subject.api_resource).to be_nil + end + end + end + + context '#transform_api_resource' do + let(:factory) do + Class.new do + def self.name + 'FooBarFactory' + end + + def api_get_path + '/foo' + end + + def api_post_path + '/bar' + end + + def api_post_body + { name: 'John Doe' } + end + + def transform_api_resource(resource) + resource[:new] = 'foobar' + resource + end + end + end + + let(:resource) { { existing: 'foo', web_url: resource_web_url } } + let(:transformed_resource) { { existing: 'foo', new: 'foobar', web_url: resource_web_url } } + + it 'transforms the resource' do + expect(subject).to receive(:post).with(resource_web_url, subject.api_post_body).and_return(raw_post) + expect(subject).to receive(:transform_api_resource).with(resource).and_return(transformed_resource) + + subject.fabricate_via_api! + end + end + end + end +end diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb index 04e04886699..184802a7903 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -1,40 +1,117 @@ +# frozen_string_literal: true + describe QA::Factory::Base do + include Support::StubENV + let(:factory) { spy('factory') } let(:product) { spy('product') } + let(:product_location) { 'http://product_location' } - describe '.fabricate!' do - subject { Class.new(described_class) } - - 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 diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb index 8aaa6665a18..657beddffb1 100644 --- a/qa/spec/factory/dependency_spec.rb +++ b/qa/spec/factory/dependency_spec.rb @@ -4,11 +4,11 @@ describe QA::Factory::Dependency do let(:block) { spy('block') } let(:signature) do - double('signature', factory: dependency, block: block) + double('signature', name: :mydep, factory: dependency, block: block) end subject do - described_class.new(:mydep, factory, signature) + described_class.new(factory, signature) end describe '#overridden?' do @@ -55,16 +55,23 @@ describe QA::Factory::Dependency do expect(factory).to have_received(:mydep=).with(dependency) end - context 'when receives a caller factory as block argument' do - let(:dependency) { QA::Factory::Base } + it 'calls given block with dependency factory and caller factory' do + expect(dependency).to receive(:fabricate!).and_yield(dependency) - it 'calls given block with dependency factory and caller factory' do - allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory) - allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any')) + subject.build! + expect(block).to have_received(:call).with(dependency, factory) + end + + context 'with no block given' do + let(:signature) do + double('signature', name: :mydep, factory: dependency, block: nil) + end + + it 'does not error' do subject.build! - expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory) + expect(dependency).to have_received(:fabricate!) end end end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb index f245aabbf43..43b1d93d769 100644 --- a/qa/spec/factory/product_spec.rb +++ b/qa/spec/factory/product_spec.rb @@ -1,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') diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb index d497d8839b8..975586b505f 100644 --- a/qa/spec/runtime/api/client_spec.rb +++ b/qa/spec/runtime/api/client_spec.rb @@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do end end - describe '#get_personal_access_token' do - it 'returns specified token from env' do - stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + describe '#personal_access_token' do + context 'when QA::Runtime::Env.personal_access_token is present' do + before do + allow(QA::Runtime::Env).to receive(:personal_access_token).and_return('a_token') + end - expect(described_class.new.get_personal_access_token).to eq 'a_token' + it 'returns specified token from env' do + expect(described_class.new.personal_access_token).to eq 'a_token' + end end - it 'returns a created token' do - allow_any_instance_of(described_class) - .to receive(:create_personal_access_token).and_return('created_token') + context 'when QA::Runtime::Env.personal_access_token is nil' do + before do + allow(QA::Runtime::Env).to receive(:personal_access_token).and_return(nil) + end - expect(described_class.new.get_personal_access_token).to eq 'created_token' + it 'returns a created token' do + expect(subject).to receive(:create_personal_access_token).and_return('created_token') + + expect(subject.personal_access_token).to eq 'created_token' + end end end end diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb index 80e3149f32d..08233e3c1d6 100644 --- a/qa/spec/runtime/api/request_spec.rb +++ b/qa/spec/runtime/api/request_spec.rb @@ -1,17 +1,23 @@ describe QA::Runtime::API::Request do - include Support::StubENV - - 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 diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index fda955f6600..b5ecf1afb80 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -34,6 +34,10 @@ describe QA::Runtime::Env do end end + describe '.verbose?' do + it_behaves_like 'boolean method', :verbose?, 'VERBOSE', false + end + describe '.signup_disabled?' do it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false end @@ -64,7 +68,54 @@ describe QA::Runtime::Env do end end + describe '.personal_access_token' do + around do |example| + described_class.instance_variable_set(:@personal_access_token, nil) + example.run + described_class.instance_variable_set(:@personal_access_token, nil) + end + + context 'when PERSONAL_ACCESS_TOKEN is set' do + before do + stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + end + + it 'returns specified token from env' do + expect(described_class.personal_access_token).to eq 'a_token' + end + end + + context 'when @personal_access_token is set' do + before do + described_class.personal_access_token = 'another_token' + end + + it 'returns the instance variable value' do + expect(described_class.personal_access_token).to eq 'another_token' + end + end + end + + describe '.personal_access_token=' do + around do |example| + described_class.instance_variable_set(:@personal_access_token, nil) + example.run + described_class.instance_variable_set(:@personal_access_token, nil) + end + + it 'saves the token' do + described_class.personal_access_token = 'a_token' + + expect(described_class.personal_access_token).to eq 'a_token' + end + end + describe '.forker?' do + before do + stub_env('GITLAB_FORKER_USERNAME', nil) + stub_env('GITLAB_FORKER_PASSWORD', nil) + end + it 'returns false if no forker credentials are defined' do expect(described_class).not_to be_forker end