diff options
Diffstat (limited to 'qa')
91 files changed, 2055 insertions, 559 deletions
@@ -18,6 +18,7 @@ module QA autoload :Address, 'qa/runtime/address' autoload :Path, 'qa/runtime/path' autoload :Fixtures, 'qa/runtime/fixtures' + autoload :Logger, 'qa/runtime/logger' module API autoload :Client, 'qa/runtime/api/client' @@ -36,8 +37,8 @@ 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' module Resource @@ -45,6 +46,7 @@ module QA autoload :Group, 'qa/factory/resource/group' autoload :Issue, 'qa/factory/resource/issue' autoload :Project, 'qa/factory/resource/project' + autoload :Label, 'qa/factory/resource/label' autoload :MergeRequest, 'qa/factory/resource/merge_request' autoload :ProjectImportedFromGithub, 'qa/factory/resource/project_imported_from_github' autoload :MergeRequestFromFork, 'qa/factory/resource/merge_request_from_fork' @@ -240,6 +242,11 @@ module QA autoload :Banner, 'qa/page/layout/banner' end + module Label + autoload :New, 'qa/page/label/new' + autoload :Index, 'qa/page/label/index' + end + module MergeRequest autoload :New, 'qa/page/merge_request/new' autoload :Show, 'qa/page/merge_request/show' @@ -317,6 +324,14 @@ module QA end end end + + # Classes that provide support to other parts of the framework. + # + module Support + module Page + autoload :Logging, 'qa/support/page/logging' + end + end end QA::Runtime::Release.extend_autoloads! diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md new file mode 100644 index 00000000000..cfce096ab39 --- /dev/null +++ b/qa/qa/factory/README.md @@ -0,0 +1,445 @@ +# 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 + + 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 + + def fabricate! + # ... same as before + 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 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 + + # ... 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 resource attribute, you can use the `attribute` method with a +block using the other factory to fabricate the resource. + +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 + attribute :name + + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-to-create-a-shirt' + end + 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 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.** + +#### 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. + +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: + +```ruby +module QA + module Factory + module Resource + class Shirt < Factory::Base + attribute :name + + 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) + attribute :brand do + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + 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! + + 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 + + brand # Eagerly construct the data + end + end + end + end +end +``` + +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. + +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 + attribute :name + + 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) + attribute :brand do + back_url = current_url + visit! + + Page::Shirt::Show.perform do |shirt_show| + shirt_show.fetch_brand_from_page + end + + visit(back_url) + 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 +from its `GET` or `POST` request. For instance, if the creation of a shirt via +the API returns + +```ruby +{ + brand: 'a-brand-new-brand', + 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. + +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 + # ... same as before + + # 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 + + # 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 + + # ... same as before + end + end + end +end +``` + +**Notes on attributes precedence:** + +- attributes from the factory have the highest precedence +- attributes from the API response take precedence over attributes from the + block (usually from Browser UI) +- attributes without a value will raise a `QA::Factory::Base::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! 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.style) # => "t-shirt" from the API response +expect(page).to have_text(my_shirt.main_fabric) # => "cotton" from the API response via the block +``` + +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! 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) # => 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 explicitly use the API fabrication method, by calling the +`.fabricate_via_api!` method: + +```ruby +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!`. + +## 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..e82e16f9415 100644 --- a/qa/qa/factory/base.rb +++ b/qa/qa/factory/base.rb @@ -1,60 +1,150 @@ +# frozen_string_literal: true + require 'forwardable' +require 'capybara/dsl' module QA module Factory class Base extend SingleForwardable + include ApiFabricator + extend Capybara::DSL + + NoValueError = Class.new(RuntimeError) - def_delegators :evaluator, :dependency, :dependencies - def_delegators :evaluator, :product, :attributes + def_delegators :evaluator, :attribute def fabricate!(*_args) raise NotImplementedError end - def self.fabricate!(*args) - new.tap do |factory| - yield factory if block_given? + def visit! + visit(web_url) + end + + private - dependencies.each do |name, signature| - Factory::Dependency.new(name, factory, signature).build! - end + 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 + fabricate_via_browser_ui!(*args, &prepare_block) + end + + def self.fabricate_via_browser_ui!(*args, &prepare_block) + options = args.extract_options! + factory = options.fetch(:factory) { new } + parents = options.fetch(:parents) { [] } + + do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do + log_fabrication(:browser_ui, factory, parents, args) { factory.fabricate!(*args) } + + current_url + end + end - factory.fabricate!(*args) + def self.fabricate_via_api!(*args, &prepare_block) + options = args.extract_options! + factory = options.fetch(:factory) { new } + parents = options.fetch(:parents) { [] } - break Factory::Product.populate!(factory) + 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 + + resource_web_url = yield + factory.web_url = resource_web_url + + Factory::Product.new(factory) + end + private_class_method :do_fabricate! + + def self.log_fabrication(method, factory, parents, args) + return yield unless Runtime::Env.debug? + + 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 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(factory, block).tap do |signature| - @dependencies.store(name, 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.store(attribute, signature) - end - end end + + attribute :web_url end end end diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb deleted file mode 100644 index fc5dc82ce29..00000000000 --- a/qa/qa/factory/dependency.rb +++ /dev/null @@ -1,39 +0,0 @@ -module QA - module Factory - class Dependency - Signature = Struct.new(:factory, :block) - - def initialize(name, factory, signature) - @name = name - @factory = factory - @signature = signature - end - - def overridden? - !!@factory.public_send(@name) - end - - def build! - 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 - end - - def fabricate! - @factory.fabricate! do |factory| - @block&.call(factory, @caller_factory) - end - end - end - end - end -end diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb index 996b7f14f61..34df0bda8e5 100644 --- a/qa/qa/factory/product.rb +++ b/qa/qa/factory/product.rb @@ -5,23 +5,28 @@ module QA class Product include Capybara::DSL - Attribute = Struct.new(:name, :block) + attr_reader :factory - def initialize - @location = current_url + def initialize(factory) + @factory = factory + + define_attributes end def visit! - visit @location + visit(web_url) + end + + def populate(*attributes) + attributes.each(&method(:public_send)) end - def self.populate!(factory) - new.tap do |product| - factory.class.attributes.each_value do |attribute| - product.instance_exec(factory, attribute.block) do |factory, block| - value = block.call(factory) - product.define_singleton_method(attribute.name) { value } - end + private + + def define_attributes + factory.class.attributes_names.each do |name| + define_singleton_method(name) do + factory.public_send(name) end end end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb index 167f47c9141..a9dfbc0a783 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/factory/repository/project_push.rb @@ -2,18 +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' - end - - product :output do |factory| - factory.output + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-code' + resource.description = 'Project with repository' + end end - product :project do |factory| - factory.project - end + attribute :output def initialize @file_name = 'file.txt' diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb index 6c5088f1da5..703c78daa99 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/factory/repository/push.rb @@ -30,6 +30,14 @@ module QA @directory = dir end + def files=(files) + if !files.is_a?(Array) || files.empty? + raise ArgumentError, "Please provide an array of hashes e.g.: [{name: 'file1', content: 'foo'}]" + end + + @files = files + end + def fabricate! Git::Repository.perform do |repository| if ssh_key @@ -63,6 +71,10 @@ module QA @directory.each_child do |f| repository.add_file(f.basename, f.read) if f.file? end + elsif @files + @files.each do |f| + repository.add_file(f[:name], f[:content]) + end else repository.add_file(file_name, file_content) end diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/factory/repository/wiki_push.rb index ecc6cc18c88..25b6ffe8323 100644 --- a/qa/qa/factory/repository/wiki_push.rb +++ b/qa/qa/factory/repository/wiki_push.rb @@ -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 diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb index f3b52565d17..b05d1e252ec 100644 --- a/qa/qa/factory/resource/branch.rb +++ b/qa/qa/factory/resource/branch.rb @@ -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| diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb index 4c53c500c27..aea99c9f80d 100644 --- a/qa/qa/factory/resource/deploy_key.rb +++ b/qa/qa/factory/resource/deploy_key.rb @@ -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| diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb index 159f79ac50b..68e98f0aa01 100644 --- a/qa/qa/factory/resource/deploy_token.rb +++ b/qa/qa/factory/resource/deploy_token.rb @@ -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! diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb index f8dea06d361..1148876c2d3 100644 --- a/qa/qa/factory/resource/file.rb +++ b/qa/qa/factory/resource/file.rb @@ -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) diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb index 83dd4000f0a..0fac4377040 100644 --- a/qa/qa/factory/resource/fork.rb +++ b/qa/qa/factory/resource/fork.rb @@ -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) { |factory| factory.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 diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb index 033fc48c08f..45e49da86f9 100644 --- a/qa/qa/factory/resource/group.rb +++ b/qa/qa/factory/resource/group.rb @@ -4,7 +4,11 @@ module QA class Group < Factory::Base attr_accessor :path, :description - dependency Factory::Resource::Sandbox, as: :sandbox + attribute :sandbox do + Factory::Resource::Sandbox.fabricate! + end + + attribute :id def initialize @path = Runtime::Namespace.name @@ -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..3a28e0d5aa6 100644 --- a/qa/qa/factory/resource/issue.rb +++ b/qa/qa/factory/resource/issue.rb @@ -2,23 +2,21 @@ module QA module Factory module Resource class Issue < Factory::Base - attr_writer :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 :title do - Page::Project::Issue::Show.act { issue_title } - end + 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) diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb index cdee35c54e3..aac6864f42f 100644 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ b/qa/qa/factory/resource/kubernetes_cluster.rb @@ -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) diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb new file mode 100644 index 00000000000..32bc519b48c --- /dev/null +++ b/qa/qa/factory/resource/label.rb @@ -0,0 +1,39 @@ +require 'securerandom' + +module QA + module Factory + module Resource + class Label < Factory::Base + attr_accessor :description, :color + + attribute :title + + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-label' + end + end + + def initialize + @title = "qa-test-#{SecureRandom.hex(8)}" + @description = 'This is a test label' + @color = '#0033CC' + end + + def fabricate! + project.visit! + + 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) + page.fill_description(@description) + page.fill_color(@color) + page.create_label + end + end + end + end + end +end diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb index ddb62bd0a68..92b8bdf4a21 100644 --- a/qa/qa/factory/resource/merge_request.rb +++ b/qa/qa/factory/resource/merge_request.rb @@ -12,31 +12,33 @@ module QA :milestone, :labels - product :project do |factory| - factory.project - end + attribute :source_branch - product :source_branch do |factory| - factory.source_branch + attribute :project do + Factory::Resource::Project.fabricate! do |resource| + resource.name = 'project-with-merge-request' + end end - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-merge-request' - end + attribute :target do + project.visit! - 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 + 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.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 @@ -50,12 +52,18 @@ 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) page.choose_milestone(@milestone) if @milestone + labels.each do |label| + page.select_label(label) + end + page.create_merge_request end end diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb index 6caaf65f673..fbe062539b9 100644 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ b/qa/qa/factory/resource/merge_request_from_fork.rb @@ -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 diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb index 166054cfcdc..ceb0f1c3d75 100644 --- a/qa/qa/factory/resource/personal_access_token.rb +++ b/qa/qa/factory/resource/personal_access_token.rb @@ -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') diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb index 90db26ab3ab..f691ae5a342 100644 --- a/qa/qa/factory/resource/project.rb +++ b/qa/qa/factory/resource/project.rb @@ -4,26 +4,24 @@ module QA module Factory module Resource class Project < Factory::Base - attr_writer :description - attr_reader :name + attribute :name + attribute :description - dependency Factory::Resource::Group, as: :group - - product :name do |factory| - factory.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 @@ -38,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 @@ -48,6 +46,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..f62092ae122 100644 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ b/qa/qa/factory/resource/project_imported_from_github.rb @@ -6,16 +6,16 @@ module QA class ProjectImportedFromGithub < Resource::Project attr_writer :personal_access_token, :github_repository_path - dependency Factory::Resource::Group, as: :group - - product :name do |factory| - factory.name + attribute :group do + Factory::Resource::Group.fabricate! end + 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 diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb index 1251ae03135..cfda58dc103 100644 --- a/qa/qa/factory/resource/project_milestone.rb +++ b/qa/qa/factory/resource/project_milestone.rb @@ -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) { |factory| factory.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) diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb index 7ac65fe6913..7108db1e55a 100644 --- a/qa/qa/factory/resource/runner.rb +++ b/qa/qa/factory/resource/runner.rb @@ -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| diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb index 5249e1755a6..56bcda9e2f3 100644 --- a/qa/qa/factory/resource/sandbox.rb +++ b/qa/qa/factory/resource/sandbox.rb @@ -6,21 +6,26 @@ module QA # creating it if it doesn't yet exist. # class Sandbox < Factory::Base + attr_reader :path + + attribute :id + attribute :path + def initialize - @name = Runtime::Namespace.sandbox_name + @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?(@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 +33,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/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb index 4084a7fc2cd..24ba3408810 100644 --- a/qa/qa/factory/resource/secret_variable.rb +++ b/qa/qa/factory/resource/secret_variable.rb @@ -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| diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb index 45236f69de9..a48a93fbe65 100644 --- a/qa/qa/factory/resource/ssh_key.rb +++ b/qa/qa/factory/resource/ssh_key.rb @@ -6,29 +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 do |factory| - factory.private_key - end - - product :title do |factory| - factory.title - end - - product :fingerprint do |factory| - factory.fingerprint - end + 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) diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb index e8b9ea2e6b4..6e6f46f7a95 100644 --- a/qa/qa/factory/resource/user.rb +++ b/qa/qa/factory/resource/user.rb @@ -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) { |factory| factory.name } - product(:username) { |factory| factory.username } - product(:email) { |factory| factory.email } - product(:password) { |factory| factory.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 diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb index acfe143fa61..769f394e85c 100644 --- a/qa/qa/factory/resource/wiki.rb +++ b/qa/qa/factory/resource/wiki.rb @@ -4,19 +4,24 @@ 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! - 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/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb index f2e58a3ea38..4e32382f910 100644 --- a/qa/qa/factory/settings/hashed_storage.rb +++ b/qa/qa/factory/settings/hashed_storage.rb @@ -5,18 +5,18 @@ 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::Main.perform do |setting| + Page::Admin::Settings::Repository.perform do |setting| setting.expand_repository_storage do |page| page.enable_hashed_storage page.save_settings end end - QA::Page::Main::Menu.act { sign_out } + QA::Page::Main::Menu.perform(&:sign_out) end end end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 14cb8125fdb..27a88534258 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -1,14 +1,24 @@ +# frozen_string_literal: true + require 'cgi' require 'uri' require 'open3' +require 'fileutils' +require 'tmpdir' module QA module Git class Repository include Scenario::Actable + attr_writer :password + attr_accessor :env_vars + def initialize - @ssh_cmd = "" + # We set HOME to the current working directory (which is a + # temporary directory created in .perform()) so the temporarily dropped + # .netrc can be utilised + self.env_vars = [%Q{HOME="#{File.dirname(netrc_file_path)}"}] end def self.perform(*args) @@ -21,36 +31,27 @@ module QA @uri = URI(address) end - def username=(name) - @username = name - @uri.user = name - end - - def password=(pass) - @password = pass - @uri.password = CGI.escape(pass).gsub('+', '%20') + def username=(username) + @username = username + @uri.user = username end def use_default_credentials - if ::QA::Runtime::User.ldap_user? - self.username = Runtime::User.ldap_username - self.password = Runtime::User.ldap_password - else - self.username = Runtime::User.username - self.password = Runtime::User.password - end + self.username, self.password = default_credentials + + add_credentials_to_netrc unless ssh_key_set? end def clone(opts = '') - run_and_redact_credentials(build_git_command("git clone #{opts} #{@uri} ./")) + run("git clone #{opts} #{uri} ./") end def checkout(branch_name) - `git checkout "#{branch_name}"` + run(%Q{git checkout "#{branch_name}"}) end def checkout_new_branch(branch_name) - `git checkout -b "#{branch_name}"` + run(%Q{git checkout -b "#{branch_name}"}) end def shallow_clone @@ -58,12 +59,10 @@ module QA end def configure_identity(name, email) - `git config user.name #{name}` - `git config user.email #{email}` - end + run(%Q{git config user.name #{name}}) + run(%Q{git config user.email #{email}}) - def configure_ssh_command(command) - @ssh_cmd = "GIT_SSH_COMMAND='#{command}'" + add_credentials_to_netrc end def commit_file(name, contents, message) @@ -74,54 +73,99 @@ module QA def add_file(name, contents) ::File.write(name, contents) - `git add #{name}` + run(%Q{git add #{name}}) end def commit(message) - `git commit -m "#{message}"` + run(%Q{git commit -m "#{message}"}) end def push_changes(branch = 'master') - output, _ = run_and_redact_credentials(build_git_command("git push #{@uri} #{branch}")) - - output + run("git push #{uri} #{branch}") end def commits - `git log --oneline`.split("\n") + run('git log --oneline').split("\n") end def use_ssh_key(key) @private_key_file = Tempfile.new("id_#{SecureRandom.hex(8)}") - File.binwrite(@private_key_file, key.private_key) - File.chmod(0700, @private_key_file) + File.binwrite(private_key_file, key.private_key) + File.chmod(0700, private_key_file) @known_hosts_file = Tempfile.new("known_hosts_#{SecureRandom.hex(8)}") keyscan_params = ['-H'] - keyscan_params << "-p #{@uri.port}" if @uri.port - keyscan_params << @uri.host - run_and_redact_credentials("ssh-keyscan #{keyscan_params.join(' ')} >> #{@known_hosts_file.path}") + keyscan_params << "-p #{uri.port}" if uri.port + keyscan_params << uri.host + run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}") - configure_ssh_command("ssh -i #{@private_key_file.path} -o UserKnownHostsFile=#{@known_hosts_file.path}") + self.env_vars << %Q{GIT_SSH_COMMAND="ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path}"} end def delete_ssh_key - return unless @private_key_file + return unless ssh_key_set? - @private_key_file.close(true) - @known_hosts_file.close(true) + private_key_file.close(true) + known_hosts_file.close(true) end - def build_git_command(command_str) - [@ssh_cmd, command_str].compact.join(' ') + private + + attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file + + def ssh_key_set? + !private_key_file.nil? end - private + def run(command_str) + command = [env_vars, command_str, '2>&1'].compact.join(' ') + Runtime::Logger.debug "Git: command=[#{command}]" + + output, _ = Open3.capture2(command) + output = output.chomp.gsub(/\s+$/, '') + Runtime::Logger.debug "Git: output=[#{output}]" + + output + end + + def default_credentials + if ::QA::Runtime::User.ldap_user? + [Runtime::User.ldap_username, Runtime::User.ldap_password] + else + [Runtime::User.username, Runtime::User.password] + end + end + + def tmp_netrc_directory + @tmp_netrc_directory ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s) + end + + def netrc_file_path + @netrc_file_path ||= File.join(tmp_netrc_directory, '.netrc') + end + + def netrc_content + "machine #{uri.host} login #{username} password #{password}" + end + + def netrc_already_contains_content? + File.exist?(netrc_file_path) && + File.readlines(netrc_file_path).grep(/^#{netrc_content}$/).any? + end + + def add_credentials_to_netrc + # Despite libcurl supporting a custom .netrc location through the + # CURLOPT_NETRC_FILE environment variable, git does not support it :( + # Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html + # + # This will create a .netrc in the correct working directory, which is + # a temporary directory created in .perform() + # + return if netrc_already_contains_content? - # Since the remote URL contains the credentials, and git occasionally - # outputs the URL. Note that stderr is redirected to stdout. - def run_and_redact_credentials(command) - Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'") + FileUtils.mkdir_p(tmp_netrc_directory) + File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) } + File.chmod(0600, netrc_file_path) 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/page/base.rb b/qa/qa/page/base.rb index 160ec58cf2c..91e229c4c8c 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -5,6 +5,7 @@ require 'capybara/dsl' module QA module Page class Base + prepend Support::Page::Logging if Runtime::Env.debug? include Capybara::DSL include Scenario::Actable extend SingleForwardable diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb new file mode 100644 index 00000000000..323acd57743 --- /dev/null +++ b/qa/qa/page/label/index.rb @@ -0,0 +1,15 @@ +module QA + module Page + module Label + class Index < Page::Base + view 'app/views/projects/labels/index.html.haml' do + element :label_create_new + end + + def go_to_new_label + click_element :label_create_new + end + end + end + end +end diff --git a/qa/qa/page/label/new.rb b/qa/qa/page/label/new.rb new file mode 100644 index 00000000000..b5422dc9400 --- /dev/null +++ b/qa/qa/page/label/new.rb @@ -0,0 +1,30 @@ +module QA + module Page + module Label + class New < Page::Base + view 'app/views/shared/labels/_form.html.haml' do + element :label_title + element :label_description + element :label_color + element :label_create_button + end + + def create_label + click_element :label_create_button + end + + def fill_title(title) + fill_element :label_title, title + end + + def fill_description(description) + fill_element :label_description, description + end + + def fill_color(color) + fill_element :label_color, color + end + end + end + end +end diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb index 83cc4bbbace..1f8f1fbca8e 100644 --- a/qa/qa/page/merge_request/new.rb +++ b/qa/qa/page/merge_request/new.rb @@ -22,6 +22,10 @@ module QA element :issuable_dropdown_menu_milestone end + view 'app/views/shared/issuable/_label_dropdown.html.haml' do + element :issuable_label + end + def create_merge_request click_element :issuable_create_button end @@ -40,6 +44,12 @@ module QA click_on milestone.title end end + + def select_label(label) + click_element :issuable_label + + click_link label.title + end end end end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index b40e90ef4ad..376606afb5d 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -23,6 +23,10 @@ module QA element :squash_checkbox end + view 'app/views/shared/issuable/_sidebar.html.haml' do + element :labels_block + end + def fast_forward_possible? !has_text?('Fast-forward merge is not possible') end @@ -64,6 +68,13 @@ module QA end end + def has_label?(label) + page.within(element_selector_css(:labels_block)) do + element = find('span', text: label) + !element.nil? + end + end + def merge! # The merge button is disabled on load wait do diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 5baf6439cfc..d688f15914c 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -4,30 +4,39 @@ module QA::Page COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running PASSED_STATUS = 'passed'.freeze - view 'app/views/shared/builds/_build_output.html.haml' do - element :build_output, '.js-build-output' # rubocop:disable QA/ElementWithPattern - element :loading_animation, '.js-build-refresh' # rubocop:disable QA/ElementWithPattern + view 'app/assets/javascripts/jobs/components/job_app.vue' do + element :loading_animation + end + + view 'app/assets/javascripts/jobs/components/job_log.vue' do + element :build_trace end view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do - element :status_badge, 'ci-status' # rubocop:disable QA/ElementWithPattern + element :status_badge end def completed? - COMPLETED_STATUSES.include? find('.ci-status').text + COMPLETED_STATUSES.include?(status_badge) end def passed? - find('.ci-status').text == PASSED_STATUS + status_badge == PASSED_STATUS end def trace_loading? - has_css?('.js-build-refresh') + has_element?(:loading_animation) end # Reminder: You may wish to wait for a particular job status before checking output def output - find('.js-build-output').text + find_element(:build_trace).text + end + + private + + def status_badge + find_element(:status_badge).text end end end diff --git a/qa/qa/page/project/menu.rb b/qa/qa/page/project/menu.rb index b32d5ea772b..cb4a10e1b6a 100644 --- a/qa/qa/page/project/menu.rb +++ b/qa/qa/page/project/menu.rb @@ -22,6 +22,7 @@ module QA element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern element :milestones_link + element :labels_link end view 'app/assets/javascripts/fly_out_nav.js' do @@ -86,6 +87,14 @@ module QA end end + def go_to_labels + hover_issues do + within_submenu do + click_element(:labels_link) + end + end + end + def click_merge_requests within_sidebar do click_link('Merge Requests') @@ -104,8 +113,22 @@ module QA end end + def click_repository + within_sidebar do + click_link('Repository') + end + end + private + def hover_issues + within_sidebar do + find_element(:issues_item).hover + + yield + end + end + def hover_settings within_sidebar do find('.qa-settings-item').hover diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index 06df1238738..b22396fd67a 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -9,7 +9,7 @@ module QA::Page element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern end - view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do + view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern element :job_link, /class.*js-pipeline-graph-job-link.*/ # rubocop:disable QA/ElementWithPattern end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index fcc4bb79c10..d6dddf03ffb 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -42,6 +42,10 @@ module QA element :web_ide_button end + view 'app/views/projects/tree/_tree_content.html.haml' do + element :file_tree + end + def project_name find('.qa-project-name').text end @@ -51,6 +55,12 @@ module QA click_element :new_file_option end + def go_to_file(filename) + within_element(:file_tree) do + click_on filename + end + end + def switch_to_branch(branch_name) find_element(:branches_select).click 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..c7052a9f300 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -1,8 +1,20 @@ +# frozen_string_literal: true + module QA module Runtime module Env extend self + attr_writer :personal_access_token + + def debug? + enabled?(ENV['QA_DEBUG'], default: false) + end + + def log_destination + ENV['QA_LOG_PATH'] || $stdout + end + # set to 'false' to have Chrome run visibly instead of headless def chrome_headless? enabled?(ENV['CHROME_HEADLESS']) @@ -22,7 +34,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 +54,7 @@ module QA end def forker? - forker_username && forker_password + !!(forker_username && forker_password) end def forker_username diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb new file mode 100644 index 00000000000..bd5c4fe5bf5 --- /dev/null +++ b/qa/qa/runtime/logger.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'logger' + +module QA + module Runtime + module Logger + extend SingleForwardable + + def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown + + singleton_class.module_eval do + attr_writer :logger + + def logger + return @logger if @logger + + @logger = ::Logger.new Runtime::Env.log_destination + @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR + @logger + end + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb index 3e3c9e859aa..ba1ba204d24 100644 --- a/qa/qa/specs/features/api/1_manage/users_spec.rb +++ b/qa/qa/specs/features/api/1_manage/users_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage do + context 'Manage' do describe 'Users API' do before(:context) do @api_client = Runtime::API::Client.new(:gitlab) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb index ae196349c6b..dae2a9e0236 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb @@ -1,5 +1,5 @@ module QA - context :manage, :smoke do + context 'Manage', :smoke do describe 'basic user login' do it 'user logs in using basic credentials' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb index 217870531da..eb9e0297287 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage, :orchestrated, :ldap do + context 'Manage', :orchestrated, :ldap do describe 'LDAP login' do it 'user logs into GitLab using LDAP credentials' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb index 6eda2c750d4..b1d641b507f 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage, :orchestrated, :mattermost do + context 'Manage', :orchestrated, :mattermost do describe 'Mattermost login' do it 'user logs into Mattermost using GitLab OAuth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) do diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb index 8d5055aab45..87f0e9030d2 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage, :orchestrated, :instance_saml do + context 'Manage', :orchestrated, :instance_saml do describe 'Instance wide SAML SSO' do it 'User logs in to gitlab with SAML SSO' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index fb6b4937554..45cb5df8252 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -16,13 +16,13 @@ module QA end end - context :manage, :skip_signup_disabled do + context 'Manage', :skip_signup_disabled do describe 'standard' do it_behaves_like 'registration and login' end end - context :manage, :orchestrated, :ldap, :skip_signup_disabled do + context 'Manage', :orchestrated, :ldap, :skip_signup_disabled do describe 'while LDAP is enabled' do it_behaves_like 'registration and login' end 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..7bf26c22fa6 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage do + context 'Manage' do describe 'Add project member' do it 'user adds project member' do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -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..a242f2158da 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 @@ -1,23 +1,21 @@ # frozen_string_literal: true module QA - context :manage, :smoke do + context 'Manage', :smoke do describe 'Project creation' do it 'user creates a new project' do 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/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index d1cd9865aef..a99b0522e73 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage, :orchestrated, :github do + context 'Manage', :orchestrated, :github do describe 'Project import from GitHub' do let(:imported_project) do Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb index 97ac35e8dba..768d40f3acf 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/view_project_activity_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage do + context 'Manage' do describe 'Project activity' do it 'user creates an event in the activity page upon Git push' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb index 49d76f31e3a..e67561b3a39 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :plan, :smoke do + context 'Plan', :smoke do describe 'Issue creation' do let(:issue_title) { 'issue title' } diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index bcf55a02a61..037ff5efbd4 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request creation' do it 'user creates a new merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -16,16 +16,26 @@ module QA milestone.project = current_project end + new_label = Factory::Resource::Label.fabricate! do |label| + label.project = current_project + label.title = 'qa-mr-test-label' + label.description = 'Merge Request label' + end + Factory::Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request with a milestone' merge_request.description = 'Great feature with milestone' merge_request.project = current_project merge_request.milestone = current_milestone + merge_request.labels.push(new_label) end - expect(page).to have_content('This is a merge request with a milestone') - expect(page).to have_content('Great feature with milestone') - expect(page).to have_content(/Opened [\w\s]+ ago/) + Page::MergeRequest::Show.perform do |merge_request| + expect(merge_request).to have_content('This is a merge request with a milestone') + expect(merge_request).to have_content('Great feature with milestone') + expect(merge_request).to have_content(/Opened [\w\s]+ ago/) + expect(merge_request).to have_label(new_label.title) + end Page::Issuable::Sidebar.perform do |sidebar| expect(sidebar).to have_milestone(current_milestone.title) diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb index 922feadb4e1..058af8aebdd 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_merge_request_from_fork_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request creation from fork' do it 'user forks a project, submits a merge request and maintainer merges it' do Runtime::Browser.visit(:gitlab, Page::Main::Login) 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..3bcf086d332 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request rebasing' do it 'user rebases source branch of merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -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/merge_request/squash_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb index b5b8855a35d..46e1005829d 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/squash_merge_request_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request squashing' do it 'user squashes commits while merging' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index c7edcf4c025..7705e12b95e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'File templates' do include Runtime::Fixtures diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb index b163ca896a7..df70b9608d9 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'SSH keys support' do let(:key_title) { "key for ssh tests #{Time.now.to_f}" } 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..9c64a9a3439 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Git clone over HTTP', :ldap do let(:location) do Page::Project::Show.act do @@ -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/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb index 82d635065a0..f65a1569fb0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Files management' do it 'user creates, edits and deletes a file via the Web' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index bf32569b6cb..b9bed39662f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Git push over HTTP', :ldap do it 'user pushes code to the repository' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb index b2da685c477..5f42cb00bd3 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_protected_branch_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Protected branch support', :ldap do let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb index 563393b3d07..36068ffba69 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/use_ssh_key_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'SSH key support' do # Note: If you run this test against GDK make sure you've enabled sshd # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md 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..07dbf39a8a3 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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Web IDE file templates' do include Runtime::Fixtures @@ -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/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb index 44dd85c1746..4126fd9fd3e 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/create_edit_clone_push_wiki_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Wiki management' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb index e901531b1bf..d66bcce879b 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/create_and_process_pipeline_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :verify, :orchestrated, :docker do + context 'Verify', :orchestrated, :docker do describe 'Pipeline creation and processing' do let(:executor) { "qa-runner-#{Time.now.to_i}" } diff --git a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb index 8d83a20f5bf..5d9aa00582f 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/runner/register_runner_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :verify, :docker do + context 'Verify', :docker do describe 'Runner registration' do let(:executor) { "qa-runner-#{Time.now.to_i}" } diff --git a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb index 08a87df5837..292f24d9c0d 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :verify do + context 'Verify' do describe 'Secret variable support' do it 'user adds a secret variable' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 17dfa887434..64b98da8bf5 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :release do + context 'Release' do describe 'Deploy key creation' do it 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb index 73af24e7f50..caf014c89ea 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/clone_using_deploy_key_spec.rb @@ -3,7 +3,7 @@ require 'digest/sha1' module QA - context :release, :docker do + context 'Release', :docker do describe 'Git clone using a deploy key' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb index e521597e07f..263ba6a6800 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :release do + context 'Release' do describe 'Deploy token creation' do it 'user adds a deploy token' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb index 3735bc00aff..40cae0793dd 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -3,7 +3,7 @@ require 'pathname' module QA - context :configure, :orchestrated, :kubernetes do + context 'Configure', :orchestrated, :kubernetes do describe 'Auto DevOps support' do after do @cluster&.remove! @@ -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! diff --git a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb index af24b36b734..7096864e011 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :configure, :orchestrated, :mattermost do + context 'Configure', :orchestrated, :mattermost do describe 'Mattermost support' do it 'user creates a group with a mattermost team' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb new file mode 100644 index 00000000000..cf5cd3a79f8 --- /dev/null +++ b/qa/qa/support/page/logging.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module QA + module Support + module Page + module Logging + def refresh + log("refreshing #{current_url}") + + super + end + + def wait(max: 60, time: 0.1, reload: true) + log("with wait: max #{max}; time #{time}; reload #{reload}") + now = Time.now + + element = super + + log("ended wait after #{Time.now - now} seconds") + + element + end + + def scroll_to(selector, text: nil) + msg = "scrolling to :#{selector}" + msg += " with text: #{text}" if text + log(msg) + + super + end + + def asset_exists?(url) + exists = super + + log("asset_exists? #{url} returned #{exists}") + + exists + end + + def find_element(name) + log("finding :#{name}") + + element = super + + log("found :#{name}") if element + + element + end + + def all_elements(name) + log("finding all :#{name}") + + elements = super + + log("found #{elements.size} :#{name}") if elements + + elements + end + + def click_element(name) + log("clicking :#{name}") + + super + end + + def fill_element(name, content) + masked_content = name.to_s.include?('password') ? '*****' : content + + log(%Q(filling :#{name} with "#{masked_content}")) + + super + end + + def has_element?(name) + found = super + + log("has_element? :#{name} returned #{found}") + + found + end + + def within_element(name) + log("within element :#{name}") + + element = super + + log("end within element :#{name}") + + element + end + + private + + def log(msg) + QA::Runtime::Logger.debug(msg) + end + end + end + end +end 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..d7b92052894 100644 --- a/qa/spec/factory/base_spec.rb +++ b/qa/spec/factory/base_spec.rb @@ -1,132 +1,248 @@ +# frozen_string_literal: true + describe QA::Factory::Base do + include Support::StubENV + let(:factory) { spy('factory') } let(:product) { spy('product') } + let(:product_location) { 'http://product_location' } - describe '.fabricate!' do - subject { Class.new(described_class) } + shared_context 'fabrication context' do + subject do + Class.new(described_class) do + def self.name + 'MyFactory' + end + end + end before do - allow(QA::Factory::Product).to receive(:new).and_return(product) - allow(QA::Factory::Product).to receive(:populate!).and_return(product) + allow(subject).to receive(:current_url).and_return(product_location) + allow(subject).to receive(:new).and_return(factory) + allow(QA::Factory::Product).to receive(:new).with(factory).and_return(product) end + end - it 'instantiates the factory and calls factory method' do - expect(subject).to receive(:new).and_return(factory) + shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil| + let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called } + + 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 - subject.fabricate!('something') + it 'does not log the factory and build method when QA_DEBUG=false' do + stub_env('QA_DEBUG', 'false') + expect(factory).to receive(fabrication_method_used).and_return(product_location) - expect(factory).to have_received(:fabricate!).with('something') + expect { subject.public_send(fabrication_method_called, 'something', factory: factory) } + .not_to output.to_stdout end + end - it 'returns fabrication product' do - allow(subject).to receive(:new).and_return(factory) + 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 - result = subject.fabricate!('something') + it 'calls .fabricate_via_browser_ui!' do + expect(described_class).to receive(:fabricate_via_browser_ui!) - expect(result).to eq product + described_class.fabricate! + end end - it 'yields factory before calling factory method' do - allow(subject).to receive(:new).and_return(factory) + context 'when factory supports fabrication via the API' do + it 'calls .fabricate_via_browser_ui!' do + expect(described_class).to receive(:fabricate_via_api!) - subject.fabricate! do |factory| - factory.something! + described_class.fabricate! end + end + end + + describe '.fabricate_via_api!' do + include_context 'fabrication context' + + it_behaves_like 'fabrication method', :fabricate_via_api! + + it 'instantiates the factory, calls factory method returns fabrication product' do + expect(factory).to receive(:fabricate_via_api!).and_return(product_location) + + result = subject.fabricate_via_api!(factory: factory, parents: []) + + expect(result).to eq(product) + end - expect(factory).to have_received(:something!).ordered - expect(factory).to have_received(:fabricate!).ordered + it 'logs the factory and build method when QA_DEBUG=true' do + stub_env('QA_DEBUG', '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 '.dependency' do - let(:dependency) { spy('dependency') } + describe '.fabricate_via_browser_ui!' do + include_context 'fabrication context' - before do - stub_const('Some::MyDependency', dependency) + it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! + + it 'instantiates the factory and calls factory method' do + subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + + expect(factory).to have_received(:fabricate!).with('something') end + it 'returns fabrication product' do + result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) + + expect(result).to eq(product) + end + + it 'logs the factory and build method when QA_DEBUG=true' do + stub_env('QA_DEBUG', 'true') + + expect { subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) } + .to output(/==> Built a MyFactory via browser_ui with args \["something"\] in [\d\w\.\-]+/) + .to_stdout + end + end + + shared_context 'simple factory' do subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep do |factory| - factory.something! + Class.new(QA::Factory::Base) do + attribute :test do + 'block' + end + + attribute :no_block + + 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 - end + let(:factory) { subject.new } + end + + describe '.attribute' do + include_context 'simple factory' - it 'defines dependency accessors' do - expect(subject.new).to respond_to :mydep, :mydep= + it 'appends new product attribute' do + expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) end - describe 'dependencies fabrication' do - let(:dependency) { double('dependency') } - let(:instance) { spy('instance') } + 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) - subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep - end + expect(result).to be_a(QA::Factory::Product) + expect(result.test).to eq('block') end + end + + context 'when the product attribute is populated via the api' do + let(:api_resource) { { no_block: 'api' } } before do - stub_const('Some::MyDependency', dependency) + 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) - allow(subject).to receive(:new).and_return(instance) - allow(instance).to receive(:mydep).and_return(nil) - allow(QA::Factory::Product).to receive(:new) - allow(QA::Factory::Product).to receive(:populate!) + expect(result).to be_a(QA::Factory::Product) + expect(result.no_block).to eq('api') end - it 'builds all dependencies first' do - expect(dependency).to receive(:fabricate!).once + context 'when the attribute also has a block in the factory' do + let(:api_resource) { { test: 'api_with_block' } } - subject.fabricate! + 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) + + 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 - end - describe '.product' do - subject do - Class.new(described_class) do - def fabricate! - "any" - end + context 'when the product attribute is populated via a factory attribute' do + before do + factory.test = 'value' + end + + it 'returns a fabrication product and defines factory attributes as its methods' do + result = subject.fabricate!(factory: factory) - # Defined only to be stubbed - def self.find_page + expect(result).to be_a(QA::Factory::Product) + expect(result.test).to eq('value') + end + + context 'when the api also has such response' do + before do + allow(factory).to receive(:api_resource).and_return({ test: 'api' }) end - product :token do - find_page.do_something_on_page! - 'resulting value' + 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 'appends new product attribute' do - expect(subject.attributes).to be_one - expect(subject.attributes).to have_key(:token) + 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) + + 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 'populating fabrication product with data' do - let(:page) { spy('page') } + describe '#web_url' do + include_context 'simple factory' - 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 'sets #web_url to #current_url after fabrication' do + subject.fabricate!(factory: factory) - it 'populates product after fabrication' do - subject.fabricate! + expect(factory.web_url).to eq(subject.current_url) + end + end - expect(product.token).to eq 'resulting value' - expect(page).to have_received(:do_something_on_page!) - end + describe '#visit!' do + include_context 'simple factory' + + before do + allow(factory).to receive(:visit) + end + + 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 diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb deleted file mode 100644 index 8aaa6665a18..00000000000 --- a/qa/spec/factory/dependency_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -describe QA::Factory::Dependency do - let(:dependency) { spy('dependency' ) } - let(:factory) { spy('factory') } - let(:block) { spy('block') } - - let(:signature) do - double('signature', factory: dependency, block: block) - end - - subject do - described_class.new(:mydep, 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 - - 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 - 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(an_instance_of(QA::Factory::Base), factory) - end - end - end - end -end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb index f245aabbf43..5b6eaa13e9c 100644 --- a/qa/spec/factory/product_spec.rb +++ b/qa/spec/factory/product_spec.rb @@ -1,36 +1,26 @@ describe QA::Factory::Product do let(:factory) do - QA::Factory::Base.new - end + Class.new(QA::Factory::Base) do + attribute :test do + 'block' + end - let(:attributes) do - { test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) } + attribute :no_block + 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 - - 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) + subject { described_class.new(factory) } - result = described_class.populate!(factory) do |instance| - instance.something = 'string' - end - - expect(result).to be product - expect(result.test).to eq('returned') - end + before do + factory.web_url = product_location end describe '.visit!' do it 'makes it possible to visit fabrication product' do allow_any_instance_of(described_class) - .to receive(:current_url).and_return('some url') - allow_any_instance_of(described_class) .to receive(:visit).and_return('visited some url') expect(subject.visit!).to eq 'visited some url' diff --git a/qa/spec/factory/repository/push_spec.rb b/qa/spec/factory/repository/push_spec.rb new file mode 100644 index 00000000000..2eb6c008248 --- /dev/null +++ b/qa/spec/factory/repository/push_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +describe QA::Factory::Repository::Push do + describe '.files=' do + let(:files) do + [ + { + name: 'file.txt', + content: 'foo' + } + ] + end + + it 'raises an error if files is not an array' do + expect { subject.files = '' }.to raise_error(ArgumentError) + end + + it 'raises an error if files is an empty array' do + expect { subject.files = [] }.to raise_error(ArgumentError) + end + + it 'does not raise if files is an array' do + expect { subject.files = files }.not_to raise_error + end + end +end diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index 53bff3bf0b3..c629f802aa4 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -1,17 +1,18 @@ describe QA::Git::Repository do + include Support::StubENV + let(:repository) { described_class.new } before do + stub_env('GITLAB_USERNAME', 'root') cd_empty_temp_directory set_bad_uri repository.use_default_credentials end describe '#clone' do - it 'redacts credentials from the URI in output' do - output, _ = repository.clone - - expect(output).to include("fatal: unable to access 'http://****@foo/bar.git/'") + it 'is unable to resolve host' do + expect(repository.clone).to include("fatal: unable to access 'http://root@foo/bar.git/'") end end @@ -20,10 +21,8 @@ describe QA::Git::Repository do `git init` # need a repo to push from end - it 'redacts credentials from the URI in output' do - output, _ = repository.push_changes - - expect(output).to include("error: failed to push some refs to 'http://****@foo/bar.git'") + it 'fails to push changes' do + expect(repository.push_changes).to include("error: failed to push some refs to 'http://root@foo/bar.git'") end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb new file mode 100644 index 00000000000..9d56353062b --- /dev/null +++ b/qa/spec/page/logging_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'capybara/dsl' + +describe QA::Support::Page::Logging do + include Support::StubENV + + let(:page) { double().as_null_object } + + before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + QA::Runtime::Logger.logger = logger + + allow(Capybara).to receive(:current_session).and_return(page) + allow(page).to receive(:current_url).and_return('http://current-url') + allow(page).to receive(:has_css?).with(any_args).and_return(true) + end + + subject do + Class.new(QA::Page::Base) do + prepend QA::Support::Page::Logging + end.new + end + + it 'logs refresh' do + expect { subject.refresh } + .to output(%r{refreshing http://current-url}).to_stdout_from_any_process + end + + it 'logs wait' do + expect { subject.wait(max: 0) {} } + .to output(/with wait/).to_stdout_from_any_process + expect { subject.wait(max: 0) {} } + .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + end + + it 'logs scroll_to' do + expect { subject.scroll_to(:element) } + .to output(/scrolling to :element/).to_stdout_from_any_process + end + + it 'logs asset_exists?' do + expect { subject.asset_exists?('http://asset-url') } + .to output(%r{asset_exists\? http://asset-url returned false}).to_stdout_from_any_process + end + + it 'logs find_element' do + expect { subject.find_element(:element) } + .to output(/found :element/).to_stdout_from_any_process + end + + it 'logs click_element' do + expect { subject.click_element(:element) } + .to output(/clicking :element/).to_stdout_from_any_process + end + + it 'logs fill_element' do + expect { subject.fill_element(:element, 'foo') } + .to output(/filling :element with "foo"/).to_stdout_from_any_process + end + + it 'logs has_element?' do + expect { subject.has_element?(:element) } + .to output(/has_element\? :element returned true/).to_stdout_from_any_process + end + + it 'logs within_element' do + expect { subject.within_element(:element) } + .to output(/within element :element/).to_stdout_from_any_process + expect { subject.within_element(:element) } + .to output(/end within element :element/).to_stdout_from_any_process + end + + context 'all_elements' do + it 'logs the number of elements found' do + allow(page).to receive(:all).and_return([1, 2]) + + expect { subject.all_elements(:element) } + .to output(/finding all :element/).to_stdout_from_any_process + expect { subject.all_elements(:element) } + .to output(/found 2 :element/).to_stdout_from_any_process + end + + it 'logs 0 if no elements are found' do + allow(page).to receive(:all).and_return([]) + + expect { subject.all_elements(:element) } + .to output(/finding all :element/).to_stdout_from_any_process + expect { subject.all_elements(:element) } + .not_to output(/found 0 :elements/).to_stdout_from_any_process + end + end +end diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb index d497d8839b8..975586b505f 100644 --- a/qa/spec/runtime/api/client_spec.rb +++ b/qa/spec/runtime/api/client_spec.rb @@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do end end - describe '#get_personal_access_token' do - it 'returns specified token from env' do - stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + describe '#personal_access_token' do + context 'when QA::Runtime::Env.personal_access_token is present' do + before do + allow(QA::Runtime::Env).to receive(:personal_access_token).and_return('a_token') + end - expect(described_class.new.get_personal_access_token).to eq 'a_token' + it 'returns specified token from env' do + expect(described_class.new.personal_access_token).to eq 'a_token' + end end - it 'returns a created token' do - allow_any_instance_of(described_class) - .to receive(:create_personal_access_token).and_return('created_token') + context 'when QA::Runtime::Env.personal_access_token is nil' do + before do + allow(QA::Runtime::Env).to receive(:personal_access_token).and_return(nil) + end - expect(described_class.new.get_personal_access_token).to eq 'created_token' + it 'returns a created token' do + expect(subject).to receive(:create_personal_access_token).and_return('created_token') + + expect(subject.personal_access_token).to eq 'created_token' + end end end end diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb index 80e3149f32d..08233e3c1d6 100644 --- a/qa/spec/runtime/api/request_spec.rb +++ b/qa/spec/runtime/api/request_spec.rb @@ -1,17 +1,23 @@ describe QA::Runtime::API::Request do - include Support::StubENV + let(:client) { QA::Runtime::API::Client.new('http://example.com') } + let(:request) { described_class.new(client, '/users') } before do - stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + allow(client).to receive(:personal_access_token).and_return('a_token') end - let(:client) { QA::Runtime::API::Client.new('http://example.com') } - let(:request) { described_class.new(client, '/users') } - describe '#url' do - it 'returns the full api request url' do + it 'returns the full API request url' do expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token' end + + context 'when oauth_access_token is passed in the query string' do + let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) } + + it 'does not adds a private_token query string' do + expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo' + end + end end describe '#request_path' do diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb deleted file mode 100644 index e69de29bb2d..00000000000 --- a/qa/spec/runtime/api_request_spec.rb +++ /dev/null diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index fda955f6600..c59c415c148 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + describe QA::Runtime::Env do include Support::StubENV @@ -38,6 +40,10 @@ describe QA::Runtime::Env do it_behaves_like 'boolean method', :signup_disabled?, 'SIGNUP_DISABLED', false end + describe '.debug?' do + it_behaves_like 'boolean method', :debug?, 'QA_DEBUG', false + end + describe '.chrome_headless?' do it_behaves_like 'boolean method', :chrome_headless?, 'CHROME_HEADLESS', true end @@ -64,7 +70,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 @@ -115,4 +168,18 @@ describe QA::Runtime::Env do expect { described_class.require_github_access_token! }.not_to raise_error end end + + describe '.log_destination' do + it 'returns $stdout if QA_LOG_PATH is not defined' do + stub_env('QA_LOG_PATH', nil) + + expect(described_class.log_destination).to eq($stdout) + end + + it 'returns the path if QA_LOG_PATH is defined' do + stub_env('QA_LOG_PATH', 'path/to_file') + + expect(described_class.log_destination).to eq('path/to_file') + end + end end diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb new file mode 100644 index 00000000000..44be3381bff --- /dev/null +++ b/qa/spec/runtime/logger_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +describe QA::Runtime::Logger do + before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + described_class.logger = logger + end + + it 'logs debug' do + expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process + end + + it 'logs info' do + expect { described_class.info('test') }.to output(/INFO -- : test/).to_stdout_from_any_process + end + + it 'logs warn' do + expect { described_class.warn('test') }.to output(/WARN -- : test/).to_stdout_from_any_process + end + + it 'logs error' do + expect { described_class.error('test') }.to output(/ERROR -- : test/).to_stdout_from_any_process + end + + it 'logs fatal' do + expect { described_class.fatal('test') }.to output(/FATAL -- : test/).to_stdout_from_any_process + end + + it 'logs unknown' do + expect { described_class.unknown('test') }.to output(/ANY -- : test/).to_stdout_from_any_process + end +end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 8e6613cd688..8e01da01340 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -3,6 +3,10 @@ require_relative '../qa' Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } RSpec.configure do |config| + config.before do |example| + QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug? + end + config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end |