From 4d0fd75cd5ceda72692a229d27ab6891fa8082e0 Mon Sep 17 00:00:00 2001 From: Lin Jen-Shin Date: Fri, 2 Nov 2018 17:32:28 +0800 Subject: Rename QA::Factory to QA::Resource * Factory::Base -> Resource::Base, and therefore: * Factory::Resource::Project -> Resource::Project --- qa/qa.rb | 60 ++- qa/qa/factory/README.md | 410 --------------------- qa/qa/factory/api_fabricator.rb | 101 ----- qa/qa/factory/base.rb | 154 -------- qa/qa/factory/repository/project_push.rb | 42 --- qa/qa/factory/repository/push.rb | 91 ----- qa/qa/factory/repository/wiki_push.rb | 34 -- qa/qa/factory/resource/branch.rb | 77 ---- qa/qa/factory/resource/ci_variable.rb | 30 -- qa/qa/factory/resource/deploy_key.rb | 43 --- qa/qa/factory/resource/deploy_token.rb | 50 --- qa/qa/factory/resource/file.rb | 38 -- qa/qa/factory/resource/fork.rb | 43 --- qa/qa/factory/resource/group.rb | 68 ---- qa/qa/factory/resource/issue.rb | 30 -- qa/qa/factory/resource/kubernetes_cluster.rb | 57 --- qa/qa/factory/resource/label.rb | 39 -- qa/qa/factory/resource/merge_request.rb | 71 ---- qa/qa/factory/resource/merge_request_from_fork.rb | 31 -- qa/qa/factory/resource/personal_access_token.rb | 27 -- qa/qa/factory/resource/project.rb | 78 ---- .../resource/project_imported_from_github.rb | 36 -- qa/qa/factory/resource/project_milestone.rb | 36 -- qa/qa/factory/resource/runner.rb | 49 --- qa/qa/factory/resource/sandbox.rb | 60 --- qa/qa/factory/resource/ssh_key.rb | 28 -- qa/qa/factory/resource/user.rb | 92 ----- qa/qa/factory/resource/wiki.rb | 30 -- qa/qa/factory/settings/hashed_storage.rb | 24 -- qa/qa/page/main/login.rb | 2 +- qa/qa/resource/README.md | 392 ++++++++++++++++++++ qa/qa/resource/api_fabricator.rb | 101 +++++ qa/qa/resource/base.rb | 154 ++++++++ qa/qa/resource/branch.rb | 77 ++++ qa/qa/resource/ci_variable.rb | 30 ++ qa/qa/resource/deploy_key.rb | 43 +++ qa/qa/resource/deploy_token.rb | 50 +++ qa/qa/resource/file.rb | 36 ++ qa/qa/resource/fork.rb | 43 +++ qa/qa/resource/group.rb | 68 ++++ qa/qa/resource/issue.rb | 30 ++ qa/qa/resource/kubernetes_cluster.rb | 57 +++ qa/qa/resource/label.rb | 39 ++ qa/qa/resource/merge_request.rb | 71 ++++ qa/qa/resource/merge_request_from_fork.rb | 31 ++ qa/qa/resource/personal_access_token.rb | 27 ++ qa/qa/resource/project.rb | 80 ++++ qa/qa/resource/project_imported_from_github.rb | 36 ++ qa/qa/resource/project_milestone.rb | 36 ++ qa/qa/resource/repository/project_push.rb | 44 +++ qa/qa/resource/repository/push.rb | 93 +++++ qa/qa/resource/repository/wiki_push.rb | 36 ++ qa/qa/resource/runner.rb | 49 +++ qa/qa/resource/sandbox.rb | 60 +++ qa/qa/resource/settings/hashed_storage.rb | 26 ++ qa/qa/resource/ssh_key.rb | 26 ++ qa/qa/resource/user.rb | 92 +++++ qa/qa/resource/wiki.rb | 30 ++ qa/qa/runtime/api/client.rb | 2 +- .../browser_ui/1_manage/login/register_spec.rb | 2 +- .../1_manage/project/add_project_member_spec.rb | 4 +- .../1_manage/project/create_project_spec.rb | 2 +- .../1_manage/project/import_github_repo_spec.rb | 2 +- .../1_manage/project/view_project_activity_spec.rb | 2 +- .../browser_ui/2_plan/issue/create_issue_spec.rb | 2 +- .../2_plan/issue/filter_issue_comments_spec.rb | 2 +- .../merge_request/create_merge_request_spec.rb | 12 +- .../merge_merge_request_from_fork_spec.rb | 2 +- .../merge_request/rebase_merge_request_spec.rb | 6 +- .../merge_request/squash_merge_request_spec.rb | 6 +- .../3_create/repository/add_file_template_spec.rb | 2 +- .../3_create/repository/add_ssh_key_spec.rb | 2 +- .../browser_ui/3_create/repository/clone_spec.rb | 2 +- .../create_edit_delete_file_via_web_spec.rb | 2 +- .../repository/push_http_private_token_spec.rb | 6 +- .../3_create/repository/push_over_http_spec.rb | 2 +- .../repository/push_protected_branch_spec.rb | 6 +- .../3_create/repository/use_ssh_key_spec.rb | 4 +- .../3_create/web_ide/add_file_template_spec.rb | 2 +- .../wiki/create_edit_clone_push_wiki_spec.rb | 4 +- .../4_verify/ci_variable/add_ci_variable_spec.rb | 2 +- .../pipeline/create_and_process_pipeline_spec.rb | 6 +- .../4_verify/runner/register_runner_spec.rb | 2 +- .../6_release/deploy_key/add_deploy_key_spec.rb | 2 +- .../deploy_key/clone_using_deploy_key_spec.rb | 10 +- .../deploy_token/add_deploy_token_spec.rb | 2 +- .../create_project_with_auto_devops_spec.rb | 8 +- qa/spec/factory/api_fabricator_spec.rb | 161 -------- qa/spec/factory/base_spec.rb | 246 ------------- qa/spec/factory/repository/push_spec.rb | 26 -- qa/spec/factory/resource/user_spec.rb | 2 +- qa/spec/resource/api_fabricator_spec.rb | 161 ++++++++ qa/spec/resource/base_spec.rb | 246 +++++++++++++ qa/spec/resource/repository/push_spec.rb | 26 ++ 94 files changed, 2375 insertions(+), 2389 deletions(-) delete mode 100644 qa/qa/factory/README.md delete mode 100644 qa/qa/factory/api_fabricator.rb delete mode 100644 qa/qa/factory/base.rb delete mode 100644 qa/qa/factory/repository/project_push.rb delete mode 100644 qa/qa/factory/repository/push.rb delete mode 100644 qa/qa/factory/repository/wiki_push.rb delete mode 100644 qa/qa/factory/resource/branch.rb delete mode 100644 qa/qa/factory/resource/ci_variable.rb delete mode 100644 qa/qa/factory/resource/deploy_key.rb delete mode 100644 qa/qa/factory/resource/deploy_token.rb delete mode 100644 qa/qa/factory/resource/file.rb delete mode 100644 qa/qa/factory/resource/fork.rb delete mode 100644 qa/qa/factory/resource/group.rb delete mode 100644 qa/qa/factory/resource/issue.rb delete mode 100644 qa/qa/factory/resource/kubernetes_cluster.rb delete mode 100644 qa/qa/factory/resource/label.rb delete mode 100644 qa/qa/factory/resource/merge_request.rb delete mode 100644 qa/qa/factory/resource/merge_request_from_fork.rb delete mode 100644 qa/qa/factory/resource/personal_access_token.rb delete mode 100644 qa/qa/factory/resource/project.rb delete mode 100644 qa/qa/factory/resource/project_imported_from_github.rb delete mode 100644 qa/qa/factory/resource/project_milestone.rb delete mode 100644 qa/qa/factory/resource/runner.rb delete mode 100644 qa/qa/factory/resource/sandbox.rb delete mode 100644 qa/qa/factory/resource/ssh_key.rb delete mode 100644 qa/qa/factory/resource/user.rb delete mode 100644 qa/qa/factory/resource/wiki.rb delete mode 100644 qa/qa/factory/settings/hashed_storage.rb create mode 100644 qa/qa/resource/README.md create mode 100644 qa/qa/resource/api_fabricator.rb create mode 100644 qa/qa/resource/base.rb create mode 100644 qa/qa/resource/branch.rb create mode 100644 qa/qa/resource/ci_variable.rb create mode 100644 qa/qa/resource/deploy_key.rb create mode 100644 qa/qa/resource/deploy_token.rb create mode 100644 qa/qa/resource/file.rb create mode 100644 qa/qa/resource/fork.rb create mode 100644 qa/qa/resource/group.rb create mode 100644 qa/qa/resource/issue.rb create mode 100644 qa/qa/resource/kubernetes_cluster.rb create mode 100644 qa/qa/resource/label.rb create mode 100644 qa/qa/resource/merge_request.rb create mode 100644 qa/qa/resource/merge_request_from_fork.rb create mode 100644 qa/qa/resource/personal_access_token.rb create mode 100644 qa/qa/resource/project.rb create mode 100644 qa/qa/resource/project_imported_from_github.rb create mode 100644 qa/qa/resource/project_milestone.rb create mode 100644 qa/qa/resource/repository/project_push.rb create mode 100644 qa/qa/resource/repository/push.rb create mode 100644 qa/qa/resource/repository/wiki_push.rb create mode 100644 qa/qa/resource/runner.rb create mode 100644 qa/qa/resource/sandbox.rb create mode 100644 qa/qa/resource/settings/hashed_storage.rb create mode 100644 qa/qa/resource/ssh_key.rb create mode 100644 qa/qa/resource/user.rb create mode 100644 qa/qa/resource/wiki.rb delete mode 100644 qa/spec/factory/api_fabricator_spec.rb delete mode 100644 qa/spec/factory/base_spec.rb delete mode 100644 qa/spec/factory/repository/push_spec.rb create mode 100644 qa/spec/resource/api_fabricator_spec.rb create mode 100644 qa/spec/resource/base_spec.rb create mode 100644 qa/spec/resource/repository/push_spec.rb (limited to 'qa') diff --git a/qa/qa.rb b/qa/qa.rb index f00331dfe93..c0d5244dbfa 100644 --- a/qa/qa.rb +++ b/qa/qa.rb @@ -36,42 +36,40 @@ module QA ## # GitLab QA fabrication mechanisms # - module Factory - autoload :ApiFabricator, 'qa/factory/api_fabricator' - autoload :Base, 'qa/factory/base' - - module Resource - autoload :Sandbox, 'qa/factory/resource/sandbox' - 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' - autoload :DeployKey, 'qa/factory/resource/deploy_key' - autoload :DeployToken, 'qa/factory/resource/deploy_token' - autoload :Branch, 'qa/factory/resource/branch' - autoload :CiVariable, 'qa/factory/resource/ci_variable' - autoload :Runner, 'qa/factory/resource/runner' - autoload :PersonalAccessToken, 'qa/factory/resource/personal_access_token' - autoload :KubernetesCluster, 'qa/factory/resource/kubernetes_cluster' - autoload :User, 'qa/factory/resource/user' - autoload :ProjectMilestone, 'qa/factory/resource/project_milestone' - autoload :Wiki, 'qa/factory/resource/wiki' - autoload :File, 'qa/factory/resource/file' - autoload :Fork, 'qa/factory/resource/fork' - autoload :SSHKey, 'qa/factory/resource/ssh_key' - end + module Resource + autoload :ApiFabricator, 'qa/resource/api_fabricator' + autoload :Base, 'qa/resource/base' + + autoload :Sandbox, 'qa/resource/sandbox' + autoload :Group, 'qa/resource/group' + autoload :Issue, 'qa/resource/issue' + autoload :Project, 'qa/resource/project' + autoload :Label, 'qa/resource/label' + autoload :MergeRequest, 'qa/resource/merge_request' + autoload :ProjectImportedFromGithub, 'qa/resource/project_imported_from_github' + autoload :MergeRequestFromFork, 'qa/resource/merge_request_from_fork' + autoload :DeployKey, 'qa/resource/deploy_key' + autoload :DeployToken, 'qa/resource/deploy_token' + autoload :Branch, 'qa/resource/branch' + autoload :CiVariable, 'qa/resource/ci_variable' + autoload :Runner, 'qa/resource/runner' + autoload :PersonalAccessToken, 'qa/resource/personal_access_token' + autoload :KubernetesCluster, 'qa/resource/kubernetes_cluster' + autoload :User, 'qa/resource/user' + autoload :ProjectMilestone, 'qa/resource/project_milestone' + autoload :Wiki, 'qa/resource/wiki' + autoload :File, 'qa/resource/file' + autoload :Fork, 'qa/resource/fork' + autoload :SSHKey, 'qa/resource/ssh_key' module Repository - autoload :Push, 'qa/factory/repository/push' - autoload :ProjectPush, 'qa/factory/repository/project_push' - autoload :WikiPush, 'qa/factory/repository/wiki_push' + autoload :Push, 'qa/resource/repository/push' + autoload :ProjectPush, 'qa/resource/repository/project_push' + autoload :WikiPush, 'qa/resource/repository/wiki_push' end module Settings - autoload :HashedStorage, 'qa/factory/settings/hashed_storage' + autoload :HashedStorage, 'qa/resource/settings/hashed_storage' end end diff --git a/qa/qa/factory/README.md b/qa/qa/factory/README.md deleted file mode 100644 index 42077f60611..00000000000 --- a/qa/qa/factory/README.md +++ /dev/null @@ -1,410 +0,0 @@ -# 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. - -#### 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 - attr_accessor :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 - attr_accessor :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 - - populate(:brand) # Eagerly construct the data - end - end - end - end -end -``` - -The `populate` method will iterate through its arguments and call each -attribute respectively. Here `populate(:brand)` has the same effect as -just `brand`. Using the populate method makes the intention clearer. - -With this, it will make sure we construct the data right after we create the -shirt. The drawback is that this will always construct the data when the resource is fabricated even if we don't need to use the data. - -Alternatively, we could just make sure we're on the right page before -constructing the brand data: - -```ruby -module QA - module Factory - module Resource - class Shirt < Factory::Base - attr_accessor :name - - 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:** - -- factory instance variables 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 deleted file mode 100644 index 887150cadf1..00000000000 --- a/qa/qa/factory/api_fabricator.rb +++ /dev/null @@ -1,101 +0,0 @@ -# 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 - process_api_response(parse_body(api_get_from(api_get_path))) - end - - def api_get_from(get_path) - url = Runtime::API::Request.new(api_client, 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 - - 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 deleted file mode 100644 index 75438b77bf3..00000000000 --- a/qa/qa/factory/base.rb +++ /dev/null @@ -1,154 +0,0 @@ -# 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, :attribute - - def fabricate!(*_args) - raise NotImplementedError - end - - def visit! - visit(web_url) - end - - def populate(*attributes) - attributes.each(&method(:public_send)) - end - - private - - def populate_attribute(name, block) - value = attribute_value(name, block) - - raise NoValueError, "No value was computed for #{name} of #{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 - - def self.fabricate_via_api!(*args, &prepare_block) - options = args.extract_options! - factory = options.fetch(:factory) { new } - parents = options.fetch(:parents) { [] } - - raise NotImplementedError unless factory.api_support? - - factory.eager_load_api_client! - - do_fabricate!(factory: factory, prepare_block: prepare_block, parents: parents) do - log_fabrication(:api, factory, parents, args) { factory.fabricate_via_api! } - end - end - - def self.do_fabricate!(factory:, prepare_block:, parents: []) - prepare_block.call(factory) if prepare_block - - resource_web_url = yield - factory.web_url = resource_web_url - - 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}" - - 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 - - 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 - end - - def attribute(name, &block) - @base.dynamic_attributes.module_eval do - attr_writer(name) - - define_method(name) do - instance_variable_get("@#{name}") || - instance_variable_set( - "@#{name}", - populate_attribute(name, block)) - end - end - end - end - - attribute :web_url - end - end -end diff --git a/qa/qa/factory/repository/project_push.rb b/qa/qa/factory/repository/project_push.rb deleted file mode 100644 index 272b7fc5818..00000000000 --- a/qa/qa/factory/repository/project_push.rb +++ /dev/null @@ -1,42 +0,0 @@ -module QA - module Factory - module Repository - class ProjectPush < Factory::Repository::Push - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-code' - resource.description = 'Project with repository' - end - end - - def initialize - @file_name = 'file.txt' - @file_content = '# This is test project' - @commit_message = "This is a test commit" - @branch_name = 'master' - @new_branch = true - end - - def repository_http_uri - @repository_http_uri ||= begin - project.visit! - Page::Project::Show.act do - choose_repository_clone_http - repository_location.uri - end - end - end - - def repository_ssh_uri - @repository_ssh_uri ||= begin - project.visit! - Page::Project::Show.act do - choose_repository_clone_ssh - repository_location.uri - end - end - end - end - end - end -end diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb deleted file mode 100644 index ffa755b9e88..00000000000 --- a/qa/qa/factory/repository/push.rb +++ /dev/null @@ -1,91 +0,0 @@ -require 'pathname' - -module QA - module Factory - module Repository - class Push < Factory::Base - attr_accessor :file_name, :file_content, :commit_message, - :branch_name, :new_branch, :output, :repository_http_uri, - :repository_ssh_uri, :ssh_key, :user - - attr_writer :remote_branch - - def initialize - @file_name = 'file.txt' - @file_content = '# This is test file' - @commit_message = "This is a test commit" - @branch_name = 'master' - @new_branch = true - @repository_http_uri = "" - @ssh_key = nil - end - - def remote_branch - @remote_branch ||= branch_name - end - - def directory=(dir) - raise "Must set directory as a Pathname" unless dir.is_a?(Pathname) - - @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 - repository.uri = repository_ssh_uri - repository.use_ssh_key(ssh_key) - else - repository.uri = repository_http_uri - repository.use_default_credentials unless user - end - - username = 'GitLab QA' - email = 'root@gitlab.com' - - if user - repository.username = user.username - repository.password = user.password - username = user.name - email = user.email - end - - repository.clone - repository.configure_identity(username, email) - - if new_branch - repository.checkout_new_branch(branch_name) - else - repository.checkout(branch_name) - end - - if @directory - @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 - - repository.commit(commit_message) - @output = repository.push_changes("#{branch_name}:#{remote_branch}") - - repository.delete_ssh_key - end - end - end - end - end -end diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/factory/repository/wiki_push.rb deleted file mode 100644 index 25b6ffe8323..00000000000 --- a/qa/qa/factory/repository/wiki_push.rb +++ /dev/null @@ -1,34 +0,0 @@ -module QA - module Factory - module Repository - class WikiPush < Factory::Repository::Push - 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 - @file_name = 'Home.md' - @file_content = '# Welcome to My Wiki' - @commit_message = 'Updating Home Page' - @branch_name = 'master' - @new_branch = false - end - - def repository_http_uri - @repository_http_uri ||= begin - wiki.visit! - Page::Project::Wiki::Show.act do - go_to_clone_repository - choose_repository_clone_http - repository_location.uri - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/branch.rb b/qa/qa/factory/resource/branch.rb deleted file mode 100644 index b05d1e252ec..00000000000 --- a/qa/qa/factory/resource/branch.rb +++ /dev/null @@ -1,77 +0,0 @@ -module QA - module Factory - module Resource - class Branch < Factory::Base - attr_accessor :project, :branch_name, - :allow_to_push, :allow_to_merge, :protected - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'protected-branch-project' - end - end - - def initialize - @branch_name = 'test/branch' - @allow_to_push = true - @allow_to_merge = true - @protected = false - end - - def fabricate! - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'kick-off.txt' - resource.commit_message = 'First commit' - end - - branch = Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.file_name = 'README.md' - resource.commit_message = 'Add readme' - resource.branch_name = 'master' - resource.new_branch = false - resource.remote_branch = @branch_name - end - - Page::Project::Show.perform do |page| - page.wait { page.has_content?(branch_name) } - end - - # The upcoming process will make it access the Protected Branches page, - # select the already created branch and protect it according - # to `allow_to_push` variable. - return branch unless @protected - - Page::Project::Menu.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_protected_branches do |page| - page.select_branch(branch_name) - - if allow_to_push - page.allow_devs_and_maintainers_to_push - else - page.allow_no_one_to_push - end - - if allow_to_merge - page.allow_devs_and_maintainers_to_merge - else - page.allow_no_one_to_merge - end - - page.wait(reload: false) do - !page.first('.btn-success').disabled? - end - - page.protect_branch - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/ci_variable.rb b/qa/qa/factory/resource/ci_variable.rb deleted file mode 100644 index a0aefc61f9f..00000000000 --- a/qa/qa/factory/resource/ci_variable.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class CiVariable < Factory::Base - attr_accessor :key, :value - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-ci-variables' - resource.description = 'project for adding CI variable test' - end - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Page::Project::Settings::CICD.perform do |setting| - setting.expand_ci_variables do |page| - page.fill_variable(key, value) - - page.save_variables - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb deleted file mode 100644 index aea99c9f80d..00000000000 --- a/qa/qa/factory/resource/deploy_key.rb +++ /dev/null @@ -1,43 +0,0 @@ -module QA - module Factory - module Resource - class DeployKey < Factory::Base - attr_accessor :title, :key - - 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 - end - end - end - - 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.perform(&:click_repository_settings) - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_keys do |page| - page.fill_key_title(title) - page.fill_key_value(key) - - page.add_key - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/deploy_token.rb b/qa/qa/factory/resource/deploy_token.rb deleted file mode 100644 index 68e98f0aa01..00000000000 --- a/qa/qa/factory/resource/deploy_token.rb +++ /dev/null @@ -1,50 +0,0 @@ -module QA - module Factory - module Resource - class DeployToken < Factory::Base - attr_accessor :name, :expires_at - - attribute :username do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_username - end - end - end - - attribute :password do - Page::Project::Settings::Repository.perform do |page| - page.expand_deploy_tokens do |token| - token.token_password - end - end - end - - 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! - project.visit! - - Page::Project::Menu.act do - click_repository_settings - end - - Page::Project::Settings::Repository.perform do |setting| - setting.expand_deploy_tokens do |page| - page.fill_token_name(name) - page.fill_token_expires_at(expires_at) - page.fill_scopes(read_repository: true, read_registry: false) - - page.add_token - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/file.rb b/qa/qa/factory/resource/file.rb deleted file mode 100644 index 1148876c2d3..00000000000 --- a/qa/qa/factory/resource/file.rb +++ /dev/null @@ -1,38 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class File < Factory::Base - attr_accessor :name, - :content, - :commit_message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-new-file' - end - end - - def initialize - @name = 'QA Test - File name' - @content = 'QA Test - File content' - @commit_message = 'QA Test - Commit message' - end - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:create_new_file!) - - Page::File::Form.perform do |page| - page.add_name(@name) - page.add_content(@content) - page.add_commit_message(@commit_message) - page.commit_changes - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb deleted file mode 100644 index d9bc44c9eb6..00000000000 --- a/qa/qa/factory/resource/fork.rb +++ /dev/null @@ -1,43 +0,0 @@ -module QA - module Factory - module Resource - class Fork < Factory::Base - attribute :push do - Factory::Repository::ProjectPush.fabricate! - end - - 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 - - def fabricate! - populate(:push, :user) - - # Sign out as admin and sign is as the fork user - Page::Main::Menu.perform(&:sign_out) - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(user) - end - - push.project.visit! - - Page::Project::Show.perform(&:fork_project) - - Page::Project::Fork::New.perform do |fork_new| - fork_new.choose_namespace(user.name) - end - - Page::Layout::Banner.perform do |page| - page.has_notice?('The project was successfully forked.') - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb deleted file mode 100644 index 45e49da86f9..00000000000 --- a/qa/qa/factory/resource/group.rb +++ /dev/null @@ -1,68 +0,0 @@ -module QA - module Factory - module Resource - class Group < Factory::Base - attr_accessor :path, :description - - attribute :sandbox do - Factory::Resource::Sandbox.fabricate! - end - - attribute :id - - def initialize - @path = Runtime::Namespace.name - @description = "QA test run at #{Runtime::Namespace.time}" - end - - def fabricate! - sandbox.visit! - - Page::Group::Show.perform do |group_show| - if group_show.has_subgroup?(path) - group_show.go_to_subgroup(path) - else - group_show.go_to_new_subgroup - - Page::Group::New.perform do |group_new| - group_new.set_path(path) - group_new.set_description(description) - group_new.set_visibility('Public') - group_new.create - end - - # Ensure that the group was actually created - group_show.wait(time: 1) do - group_show.has_text?(path) && - group_show.has_new_project_or_subgroup_dropdown? - end - 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 -end diff --git a/qa/qa/factory/resource/issue.rb b/qa/qa/factory/resource/issue.rb deleted file mode 100644 index 3a28e0d5aa6..00000000000 --- a/qa/qa/factory/resource/issue.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Issue < Factory::Base - attr_writer :description - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-issues' - resource.description = 'project for adding issues' - end - end - - attribute :title - - def fabricate! - project.visit! - - Page::Project::Show.perform(&:go_to_new_issue) - - Page::Project::Issue::New.perform do |page| - page.add_title(@title) - page.add_description(@description) - page.create_new_issue - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/kubernetes_cluster.rb b/qa/qa/factory/resource/kubernetes_cluster.rb deleted file mode 100644 index aac6864f42f..00000000000 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ /dev/null @@ -1,57 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class KubernetesCluster < Factory::Base - attr_writer :project, :cluster, - :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner - - attribute :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) - end - - def fabricate! - @project.visit! - - Page::Project::Menu.perform( - &:click_operations_kubernetes) - - Page::Project::Operations::Kubernetes::Index.perform( - &:add_kubernetes_cluster) - - Page::Project::Operations::Kubernetes::Add.perform( - &:add_existing_cluster) - - Page::Project::Operations::Kubernetes::AddExisting.perform do |page| - page.set_cluster_name(@cluster.cluster_name) - page.set_api_url(@cluster.api_url) - page.set_ca_certificate(@cluster.ca_certificate) - page.set_token(@cluster.token) - page.check_rbac! if @cluster.rbac - page.add_cluster! - end - - if @install_helm_tiller - Page::Project::Operations::Kubernetes::Show.perform do |page| - # We must wait a few seconds for permissions to be set up correctly for new cluster - sleep 10 - - # Helm must be installed before everything else - page.install!(:helm) - page.await_installed(:helm) - - page.install!(:ingress) if @install_ingress - page.install!(:prometheus) if @install_prometheus - page.install!(:runner) if @install_runner - - page.await_installed(:ingress) if @install_ingress - page.await_installed(:prometheus) if @install_prometheus - page.await_installed(:runner) if @install_runner - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/label.rb b/qa/qa/factory/resource/label.rb deleted file mode 100644 index 32bc519b48c..00000000000 --- a/qa/qa/factory/resource/label.rb +++ /dev/null @@ -1,39 +0,0 @@ -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 deleted file mode 100644 index 4b7d2287f98..00000000000 --- a/qa/qa/factory/resource/merge_request.rb +++ /dev/null @@ -1,71 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class MergeRequest < Factory::Base - attr_accessor :title, - :description, - :source_branch, - :target_branch, - :assignee, - :milestone, - :labels - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-with-merge-request' - end - end - - attribute :target do - project.visit! - - Factory::Repository::ProjectPush.fabricate! do |resource| - resource.project = project - resource.branch_name = 'master' - resource.remote_branch = target_branch - end - end - - 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 - @title = 'QA test - merge request' - @description = 'This is a test merge request' - @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" - @target_branch = "master" - @assignee = nil - @milestone = nil - @labels = [] - end - - def fabricate! - populate(:target, :source) - - project.visit! - 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 - end - end - end -end diff --git a/qa/qa/factory/resource/merge_request_from_fork.rb b/qa/qa/factory/resource/merge_request_from_fork.rb deleted file mode 100644 index 1311bf625a6..00000000000 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ /dev/null @@ -1,31 +0,0 @@ -module QA - module Factory - module Resource - class MergeRequestFromFork < MergeRequest - attr_accessor :fork_branch - - attribute :fork do - Factory::Resource::Fork.fabricate! - end - - 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! - populate(:push) - - fork.visit! - - Page::Project::Show.perform(&:new_merge_request) - Page::MergeRequest::New.perform(&:create_merge_request) - end - end - end - end -end diff --git a/qa/qa/factory/resource/personal_access_token.rb b/qa/qa/factory/resource/personal_access_token.rb deleted file mode 100644 index ceb0f1c3d75..00000000000 --- a/qa/qa/factory/resource/personal_access_token.rb +++ /dev/null @@ -1,27 +0,0 @@ -module QA - module Factory - module Resource - ## - # Create a personal access token that can be used by the api - # - class PersonalAccessToken < Factory::Base - attr_accessor :name - - attribute :access_token do - Page::Profile::PersonalAccessTokens.perform(&:created_access_token) - end - - def fabricate! - 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') - page.check_api - page.create_token - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb deleted file mode 100644 index f691ae5a342..00000000000 --- a/qa/qa/factory/resource/project.rb +++ /dev/null @@ -1,78 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Project < Factory::Base - attribute :name - attribute :description - - attribute :group do - Factory::Resource::Group.fabricate! - end - - attribute :repository_ssh_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_ssh - page.repository_location - end - end - - attribute :repository_http_location do - Page::Project::Show.perform do |page| - page.choose_repository_clone_http - page.repository_location - end - end - - def initialize - @description = 'My awesome project' - end - - def name=(raw_name) - @name = "#{raw_name}-#{SecureRandom.hex(8)}" - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.choose_test_namespace - page.choose_name(@name) - page.add_description(@description) - page.set_visibility('Public') - 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 -end diff --git a/qa/qa/factory/resource/project_imported_from_github.rb b/qa/qa/factory/resource/project_imported_from_github.rb deleted file mode 100644 index ce20641e6cc..00000000000 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ /dev/null @@ -1,36 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class ProjectImportedFromGithub < Resource::Project - attr_accessor :name - attr_writer :personal_access_token, :github_repository_path - - attribute :group do - Factory::Resource::Group.fabricate! - end - - def fabricate! - group.visit! - - Page::Group::Show.perform(&:go_to_new_project) - - Page::Project::New.perform do |page| - page.go_to_import_project - end - - Page::Project::New.perform do |page| - page.go_to_github_import - end - - Page::Project::Import::Github.perform do |page| - page.add_personal_access_token(@personal_access_token) - page.list_repos - page.import!(@github_repository_path, @name) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/project_milestone.rb b/qa/qa/factory/resource/project_milestone.rb deleted file mode 100644 index 383f534c12c..00000000000 --- a/qa/qa/factory/resource/project_milestone.rb +++ /dev/null @@ -1,36 +0,0 @@ -module QA - module Factory - module Resource - class ProjectMilestone < Factory::Base - attr_reader :title - attr_accessor :description - - attribute :project do - Factory::Resource::Project.fabricate! - end - - def title=(title) - @title = "#{title}-#{SecureRandom.hex(4)}" - @description = 'A milestone' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform do |page| - page.click_issues - page.click_milestones - end - - Page::Project::Milestone::Index.perform(&:click_new_milestone) - - Page::Project::Milestone::New.perform do |milestone_new| - milestone_new.set_title(@title) - milestone_new.set_description(@description) - milestone_new.create_new_milestone - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/runner.rb b/qa/qa/factory/resource/runner.rb deleted file mode 100644 index 7108db1e55a..00000000000 --- a/qa/qa/factory/resource/runner.rb +++ /dev/null @@ -1,49 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Runner < Factory::Base - attr_writer :name, :tags, :image - - 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 - @name || "qa-runner-#{SecureRandom.hex(4)}" - end - - def tags - @tags || %w[qa e2e] - end - - def image - @image || 'gitlab/gitlab-runner:alpine' - end - - def fabricate! - project.visit! - - Page::Project::Menu.perform(&:click_ci_cd_settings) - - Service::Runner.new(name).tap do |runner| - Page::Project::Settings::CICD.perform do |settings| - settings.expand_runners_settings do |runners| - runner.pull - runner.token = runners.registration_token - runner.address = runners.coordinator_address - runner.tags = tags - runner.image = image - runner.register! - end - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb deleted file mode 100644 index a125bac65dd..00000000000 --- a/qa/qa/factory/resource/sandbox.rb +++ /dev/null @@ -1,60 +0,0 @@ -module QA - module Factory - module Resource - ## - # Ensure we're in our sandbox namespace, either by navigating to it or by - # creating it if it doesn't yet exist. - # - class Sandbox < Factory::Base - attr_reader :path - - attribute :id - - def initialize - @path = Runtime::Namespace.sandbox_name - end - - def fabricate! - Page::Main::Menu.perform(&:go_to_groups) - - Page::Dashboard::Groups.perform do |page| - 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(path) - group.set_description('GitLab QA Sandbox Group') - group.set_visibility('Public') - group.create - end - 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 -end diff --git a/qa/qa/factory/resource/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb deleted file mode 100644 index 6f952eda36f..00000000000 --- a/qa/qa/factory/resource/ssh_key.rb +++ /dev/null @@ -1,28 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class SSHKey < Factory::Base - extend Forwardable - - attr_accessor :title - - def_delegators :key, :private_key, :public_key, :fingerprint - - def key - @key ||= Runtime::Key::RSA.new - end - - def fabricate! - 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) - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb deleted file mode 100644 index 68faadddd1c..00000000000 --- a/qa/qa/factory/resource/user.rb +++ /dev/null @@ -1,92 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class User < Factory::Base - attr_reader :unique_id - attr_writer :username, :password - - def initialize - @unique_id = SecureRandom.hex(8) - end - - def username - @username ||= "qa-user-#{unique_id}" - end - - def password - @password ||= 'password' - end - - def name - @name ||= username - end - - def email - @email ||= "#{username}@example.com" - end - - def credentials_given? - defined?(@username) && defined?(@password) - end - - def fabricate! - # Don't try to log-out if we're not logged-in - if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } - Page::Main::Menu.perform { |main| main.sign_out } - end - - if credentials_given? - Page::Main::Login.perform do |login| - login.sign_in_using_credentials(self) - end - else - Page::Main::Login.perform do |login| - login.switch_to_register_tab - end - Page::Main::SignUp.perform do |signup| - signup.sign_up!(self) - end - end - end - - def fabricate_via_api! - resource_web_url(api_get) - rescue ResourceNotFoundError - super - end - - def api_get_path - "/users/#{fetch_id(username)}" - end - - def api_post_path - '/users' - end - - def api_post_body - { - email: email, - password: password, - username: username, - name: name, - skip_confirmation: true - } - end - - private - - def fetch_id(username) - users = parse_body(api_get_from("/users?username=#{username}")) - - unless users.size == 1 && users.first[:username] == username - raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`." - end - - users.first[:id] - end - end - end - end -end diff --git a/qa/qa/factory/resource/wiki.rb b/qa/qa/factory/resource/wiki.rb deleted file mode 100644 index 769f394e85c..00000000000 --- a/qa/qa/factory/resource/wiki.rb +++ /dev/null @@ -1,30 +0,0 @@ -module QA - module Factory - module Resource - class Wiki < Factory::Base - attr_accessor :title, :content, :message - - attribute :project do - Factory::Resource::Project.fabricate! do |resource| - resource.name = 'project-for-wikis' - resource.description = 'project for adding wikis' - end - end - - def fabricate! - 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 - end - end -end diff --git a/qa/qa/factory/settings/hashed_storage.rb b/qa/qa/factory/settings/hashed_storage.rb deleted file mode 100644 index 4e32382f910..00000000000 --- a/qa/qa/factory/settings/hashed_storage.rb +++ /dev/null @@ -1,24 +0,0 @@ -module QA - module Factory - module Settings - class HashedStorage < Factory::Base - def fabricate!(*traits) - raise ArgumentError unless traits.include?(:enabled) - - Page::Main::Login.perform(&:sign_in_using_credentials) - Page::Main::Menu.perform(&:go_to_admin_area) - Page::Admin::Menu.perform(&:go_to_repository_settings) - - Page::Admin::Settings::Repository.perform do |setting| - setting.expand_repository_storage do |page| - page.enable_hashed_storage - page.save_settings - end - end - - QA::Page::Main::Menu.perform(&:sign_out) - end - end - end - end -end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index 94b9486b0d5..97ffe0e5716 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -65,7 +65,7 @@ module QA end def sign_in_using_admin_credentials - admin = QA::Factory::Resource::User.new.tap do |user| + admin = QA::Resource::User.new.tap do |user| user.username = QA::Runtime::User.admin_username user.password = QA::Runtime::User.admin_password end diff --git a/qa/qa/resource/README.md b/qa/qa/resource/README.md new file mode 100644 index 00000000000..4cdeb3f42a2 --- /dev/null +++ b/qa/qa/resource/README.md @@ -0,0 +1,392 @@ +# Resource class in GitLab QA + +Resources are primarily created using Browser UI steps, but can also +be created via the API. + +## How to properly implement a resource class? + +All resource classes should inherit from [`Resource::Base`](./base.rb). + +There is only one mandatory method to implement to define a resource class. +This is the `#fabricate!` method, which is used to build the 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 Resource + class Shirt < 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 +``` + +### Define API implementation + +A resource class may also implement the three following methods to be able to +create the 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` resource class, and add these three API methods: + +```ruby +module QA + module Resource + class Shirt < 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 +``` + +The [`Project` resource](./project.rb) is a good real example of Browser +UI and API implementations. + +#### 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 resource class 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` resource class, and add a `project` attribute to it: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + 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 +``` + +**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` resource class, and define a `:brand` attribute: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + 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 +``` + +**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::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::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 Resource + class Shirt < 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 + + populate(:brand) # Eagerly construct the data + end + end + end +end +``` + +The `populate` method will iterate through its arguments and call each +attribute respectively. Here `populate(:brand)` has the same effect as +just `brand`. Using the populate method makes the intention clearer. + +With this, it will make sure we construct the data right after we create the +shirt. The drawback is that this will always construct the data when the +resource is fabricated even if we don't need to use the data. + +Alternatively, we could just make sure we're on the right page before +constructing the brand data: + +```ruby +module QA + module Resource + class Shirt < Base + attr_accessor :name + + attribute :project do + 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 +``` + +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` resource class, and define a `:style` and a +`:main_fabric` attributes: + +```ruby +module QA + module Resource + class Shirt < Base + # ... same as before + + # @style from the instance if present, + # or fetched from the API response if present, + # or a QA::Resource::Base::NoValueError is raised otherwise + attribute :style + + # If @main_fabric 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, and + # store the result in @main_fabric + attribute :main_fabric do + api_response.&dig(:materials, 0, 0) + end + + # ... same as before + end + end +end +``` + +**Notes on attributes precedence:** + +- resource instance variables 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::Resource::Base::NoValueError` error + +## Creating resources in your tests + +To create a resource in your tests, you can call the `.fabricate!` method on +the resource class. +Note that if the resource class 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` resource class: + +```ruby +my_shirt = Resource::Shirt.fabricate! do |shirt| + shirt.name = 'my-shirt' +end + +expect(page).to have_text(my_shirt.name) # => "my-shirt" from the resource's instance variable +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 = 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 resource's instance variable +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::Resource::Base::NoValueError will be raised because no API response nor a block is provided +expect(page).to have_text(my_shirt.main_fabric) # => QA::Resource::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 = Resource::Shirt.fabricate_via_api! do |shirt| + shirt.name = 'my-shirt' +end +``` + +In this case, the result will be similar to calling +`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/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb new file mode 100644 index 00000000000..3762a94f312 --- /dev/null +++ b/qa/qa/resource/api_fabricator.rb @@ -0,0 +1,101 @@ +# frozen_string_literal: true + +require 'airborne' +require 'active_support/core_ext/object/deep_dup' +require 'capybara/dsl' + +module QA + module Resource + 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, "Resource #{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 + process_api_response(parse_body(api_get_from(api_get_path))) + end + + def api_get_from(get_path) + url = Runtime::API::Request.new(api_client, 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 + + 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(api_resource) + api_resource + end + end + end +end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb new file mode 100644 index 00000000000..f3eefb70520 --- /dev/null +++ b/qa/qa/resource/base.rb @@ -0,0 +1,154 @@ +# frozen_string_literal: true + +require 'forwardable' +require 'capybara/dsl' + +module QA + module Resource + class Base + extend SingleForwardable + include ApiFabricator + extend Capybara::DSL + + NoValueError = Class.new(RuntimeError) + + def_delegators :evaluator, :attribute + + def fabricate!(*_args) + raise NotImplementedError + end + + def visit! + visit(web_url) + end + + def populate(*attributes) + attributes.each(&method(:public_send)) + end + + private + + def populate_attribute(name, block) + value = attribute_value(name, block) + + raise NoValueError, "No value was computed for #{name} of #{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! + resource = options.fetch(:resource) { new } + parents = options.fetch(:parents) { [] } + + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) } + + current_url + end + end + + def self.fabricate_via_api!(*args, &prepare_block) + options = args.extract_options! + resource = options.fetch(:resource) { new } + parents = options.fetch(:parents) { [] } + + raise NotImplementedError unless resource.api_support? + + resource.eager_load_api_client! + + do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do + log_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! } + end + end + + def self.do_fabricate!(resource:, prepare_block:, parents: []) + prepare_block.call(resource) if prepare_block + + resource_web_url = yield + resource.web_url = resource_web_url + + resource + end + private_class_method :do_fabricate! + + def self.log_fabrication(method, resource, 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}" + + 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 ||= Base::DSL.new(self) + end + private_class_method :evaluator + + 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 + end + + def attribute(name, &block) + @base.dynamic_attributes.module_eval do + attr_writer(name) + + define_method(name) do + instance_variable_get("@#{name}") || + instance_variable_set( + "@#{name}", + populate_attribute(name, block)) + end + end + end + end + + attribute :web_url + end + end +end diff --git a/qa/qa/resource/branch.rb b/qa/qa/resource/branch.rb new file mode 100644 index 00000000000..bd52c4abe02 --- /dev/null +++ b/qa/qa/resource/branch.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + module Resource + class Branch < Base + attr_accessor :project, :branch_name, + :allow_to_push, :allow_to_merge, :protected + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'protected-branch-project' + end + end + + def initialize + @branch_name = 'test/branch' + @allow_to_push = true + @allow_to_merge = true + @protected = false + end + + def fabricate! + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'kick-off.txt' + resource.commit_message = 'First commit' + end + + branch = Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.file_name = 'README.md' + resource.commit_message = 'Add readme' + resource.branch_name = 'master' + resource.new_branch = false + resource.remote_branch = @branch_name + end + + Page::Project::Show.perform do |page| + page.wait { page.has_content?(branch_name) } + end + + # The upcoming process will make it access the Protected Branches page, + # select the already created branch and protect it according + # to `allow_to_push` variable. + return branch unless @protected + + Page::Project::Menu.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_protected_branches do |page| + page.select_branch(branch_name) + + if allow_to_push + page.allow_devs_and_maintainers_to_push + else + page.allow_no_one_to_push + end + + if allow_to_merge + page.allow_devs_and_maintainers_to_merge + else + page.allow_no_one_to_merge + end + + page.wait(reload: false) do + !page.first('.btn-success').disabled? + end + + page.protect_branch + end + end + end + end + end +end diff --git a/qa/qa/resource/ci_variable.rb b/qa/qa/resource/ci_variable.rb new file mode 100644 index 00000000000..0570c47d41c --- /dev/null +++ b/qa/qa/resource/ci_variable.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class CiVariable < Base + attr_accessor :key, :value + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-variables' + resource.description = 'project for adding CI variable test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Page::Project::Settings::CICD.perform do |setting| + setting.expand_ci_variables do |page| + page.fill_variable(key, value) + + page.save_variables + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb new file mode 100644 index 00000000000..9ed8fb7726e --- /dev/null +++ b/qa/qa/resource/deploy_key.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployKey < Base + attr_accessor :title, :key + + 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 + end + end + end + + attribute :project do + 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.perform(&:click_repository_settings) + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_keys do |page| + page.fill_key_title(title) + page.fill_key_value(key) + + page.add_key + end + end + end + end + end +end diff --git a/qa/qa/resource/deploy_token.rb b/qa/qa/resource/deploy_token.rb new file mode 100644 index 00000000000..cee4422f6b4 --- /dev/null +++ b/qa/qa/resource/deploy_token.rb @@ -0,0 +1,50 @@ +# frozen_string_literal: true + +module QA + module Resource + class DeployToken < Base + attr_accessor :name, :expires_at + + attribute :username do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_username + end + end + end + + attribute :password do + Page::Project::Settings::Repository.perform do |page| + page.expand_deploy_tokens do |token| + token.token_password + end + end + end + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-to-deploy' + resource.description = 'project for adding deploy token test' + end + end + + def fabricate! + project.visit! + + Page::Project::Menu.act do + click_repository_settings + end + + Page::Project::Settings::Repository.perform do |setting| + setting.expand_deploy_tokens do |page| + page.fill_token_name(name) + page.fill_token_expires_at(expires_at) + page.fill_scopes(read_repository: true, read_registry: false) + + page.add_token + end + end + end + end + end +end diff --git a/qa/qa/resource/file.rb b/qa/qa/resource/file.rb new file mode 100644 index 00000000000..effc5a7940b --- /dev/null +++ b/qa/qa/resource/file.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class File < Base + attr_accessor :name, + :content, + :commit_message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-new-file' + end + end + + def initialize + @name = 'QA Test - File name' + @content = 'QA Test - File content' + @commit_message = 'QA Test - Commit message' + end + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:create_new_file!) + + Page::File::Form.perform do |page| + page.add_name(@name) + page.add_content(@content) + page.add_commit_message(@commit_message) + page.commit_changes + end + end + end + end +end diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb new file mode 100644 index 00000000000..9fd66f3a36a --- /dev/null +++ b/qa/qa/resource/fork.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +module QA + module Resource + class Fork < Base + attribute :push do + Repository::ProjectPush.fabricate! + end + + attribute :user do + User.fabricate! do |resource| + if Runtime::Env.forker? + resource.username = Runtime::Env.forker_username + resource.password = Runtime::Env.forker_password + end + end + end + + def fabricate! + populate(:push, :user) + + # Sign out as admin and sign is as the fork user + Page::Main::Menu.perform(&:sign_out) + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(user) + end + + push.project.visit! + + Page::Project::Show.perform(&:fork_project) + + Page::Project::Fork::New.perform do |fork_new| + fork_new.choose_namespace(user.name) + end + + Page::Layout::Banner.perform do |page| + page.has_notice?('The project was successfully forked.') + end + end + end + end +end diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb new file mode 100644 index 00000000000..dce15e4f10b --- /dev/null +++ b/qa/qa/resource/group.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module QA + module Resource + class Group < Base + attr_accessor :path, :description + + attribute :sandbox do + Sandbox.fabricate! + end + + attribute :id + + def initialize + @path = Runtime::Namespace.name + @description = "QA test run at #{Runtime::Namespace.time}" + end + + def fabricate! + sandbox.visit! + + Page::Group::Show.perform do |group_show| + if group_show.has_subgroup?(path) + group_show.go_to_subgroup(path) + else + group_show.go_to_new_subgroup + + Page::Group::New.perform do |group_new| + group_new.set_path(path) + group_new.set_description(description) + group_new.set_visibility('Public') + group_new.create + end + + # Ensure that the group was actually created + group_show.wait(time: 1) do + group_show.has_text?(path) && + group_show.has_new_project_or_subgroup_dropdown? + end + 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/resource/issue.rb b/qa/qa/resource/issue.rb new file mode 100644 index 00000000000..2c2f27fe231 --- /dev/null +++ b/qa/qa/resource/issue.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Issue < Base + attr_writer :description + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-issues' + resource.description = 'project for adding issues' + end + end + + attribute :title + + def fabricate! + project.visit! + + Page::Project::Show.perform(&:go_to_new_issue) + + Page::Project::Issue::New.perform do |page| + page.add_title(@title) + page.add_description(@description) + page.create_new_issue + end + end + end + end +end diff --git a/qa/qa/resource/kubernetes_cluster.rb b/qa/qa/resource/kubernetes_cluster.rb new file mode 100644 index 00000000000..96c8843fb99 --- /dev/null +++ b/qa/qa/resource/kubernetes_cluster.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class KubernetesCluster < Base + attr_writer :project, :cluster, + :install_helm_tiller, :install_ingress, :install_prometheus, :install_runner + + attribute :ingress_ip do + Page::Project::Operations::Kubernetes::Show.perform(&:ingress_ip) + end + + def fabricate! + @project.visit! + + Page::Project::Menu.perform( + &:click_operations_kubernetes) + + Page::Project::Operations::Kubernetes::Index.perform( + &:add_kubernetes_cluster) + + Page::Project::Operations::Kubernetes::Add.perform( + &:add_existing_cluster) + + Page::Project::Operations::Kubernetes::AddExisting.perform do |page| + page.set_cluster_name(@cluster.cluster_name) + page.set_api_url(@cluster.api_url) + page.set_ca_certificate(@cluster.ca_certificate) + page.set_token(@cluster.token) + page.check_rbac! if @cluster.rbac + page.add_cluster! + end + + if @install_helm_tiller + Page::Project::Operations::Kubernetes::Show.perform do |page| + # We must wait a few seconds for permissions to be set up correctly for new cluster + sleep 10 + + # Helm must be installed before everything else + page.install!(:helm) + page.await_installed(:helm) + + page.install!(:ingress) if @install_ingress + page.install!(:prometheus) if @install_prometheus + page.install!(:runner) if @install_runner + + page.await_installed(:ingress) if @install_ingress + page.await_installed(:prometheus) if @install_prometheus + page.await_installed(:runner) if @install_runner + end + end + end + end + end +end diff --git a/qa/qa/resource/label.rb b/qa/qa/resource/label.rb new file mode 100644 index 00000000000..c0869cb1f2a --- /dev/null +++ b/qa/qa/resource/label.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Label < Base + attr_accessor :description, :color + + attribute :title + + attribute :project do + 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 diff --git a/qa/qa/resource/merge_request.rb b/qa/qa/resource/merge_request.rb new file mode 100644 index 00000000000..466a7942dc6 --- /dev/null +++ b/qa/qa/resource/merge_request.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class MergeRequest < Base + attr_accessor :title, + :description, + :source_branch, + :target_branch, + :assignee, + :milestone, + :labels + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-merge-request' + end + end + + attribute :target do + project.visit! + + Repository::ProjectPush.fabricate! do |resource| + resource.project = project + resource.branch_name = 'master' + resource.remote_branch = target_branch + end + end + + attribute :source do + 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 + @title = 'QA test - merge request' + @description = 'This is a test merge request' + @source_branch = "qa-test-feature-#{SecureRandom.hex(8)}" + @target_branch = "master" + @assignee = nil + @milestone = nil + @labels = [] + end + + def fabricate! + populate(:target, :source) + + project.visit! + 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 + end + end +end diff --git a/qa/qa/resource/merge_request_from_fork.rb b/qa/qa/resource/merge_request_from_fork.rb new file mode 100644 index 00000000000..f91ae299d76 --- /dev/null +++ b/qa/qa/resource/merge_request_from_fork.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + module Resource + class MergeRequestFromFork < MergeRequest + attr_accessor :fork_branch + + attribute :fork do + Fork.fabricate! + end + + attribute :push do + 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! + populate(:push) + + fork.visit! + + Page::Project::Show.perform(&:new_merge_request) + Page::MergeRequest::New.perform(&:create_merge_request) + end + end + end +end diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb new file mode 100644 index 00000000000..b8dd0a3562f --- /dev/null +++ b/qa/qa/resource/personal_access_token.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Create a personal access token that can be used by the api + # + class PersonalAccessToken < Base + attr_accessor :name + + attribute :access_token do + Page::Profile::PersonalAccessTokens.perform(&:created_access_token) + end + + def fabricate! + 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') + page.check_api + page.create_token + end + end + end + end +end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb new file mode 100644 index 00000000000..7fdf69278f9 --- /dev/null +++ b/qa/qa/resource/project.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Project < Base + attribute :name + attribute :description + + attribute :group do + Group.fabricate! + end + + attribute :repository_ssh_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_ssh + page.repository_location + end + end + + attribute :repository_http_location do + Page::Project::Show.perform do |page| + page.choose_repository_clone_http + page.repository_location + end + end + + def initialize + @description = 'My awesome project' + end + + def name=(raw_name) + @name = "#{raw_name}-#{SecureRandom.hex(8)}" + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.choose_test_namespace + page.choose_name(@name) + page.add_description(@description) + page.set_visibility('Public') + 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(api_resource) + api_resource[:repository_ssh_location] = + Git::Location.new(api_resource[:ssh_url_to_repo]) + api_resource[:repository_http_location] = + Git::Location.new(api_resource[:http_url_to_repo]) + api_resource + end + end + end +end diff --git a/qa/qa/resource/project_imported_from_github.rb b/qa/qa/resource/project_imported_from_github.rb new file mode 100644 index 00000000000..3f02fe885a9 --- /dev/null +++ b/qa/qa/resource/project_imported_from_github.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class ProjectImportedFromGithub < Project + attr_accessor :name + attr_writer :personal_access_token, :github_repository_path + + attribute :group do + Group.fabricate! + end + + def fabricate! + group.visit! + + Page::Group::Show.perform(&:go_to_new_project) + + Page::Project::New.perform do |page| + page.go_to_import_project + end + + Page::Project::New.perform do |page| + page.go_to_github_import + end + + Page::Project::Import::Github.perform do |page| + page.add_personal_access_token(@personal_access_token) + page.list_repos + page.import!(@github_repository_path, @name) + end + end + end + end +end diff --git a/qa/qa/resource/project_milestone.rb b/qa/qa/resource/project_milestone.rb new file mode 100644 index 00000000000..a4d6657caff --- /dev/null +++ b/qa/qa/resource/project_milestone.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + class ProjectMilestone < Base + attr_reader :title + attr_accessor :description + + attribute :project do + Project.fabricate! + end + + def title=(title) + @title = "#{title}-#{SecureRandom.hex(4)}" + @description = 'A milestone' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform do |page| + page.click_issues + page.click_milestones + end + + Page::Project::Milestone::Index.perform(&:click_new_milestone) + + Page::Project::Milestone::New.perform do |milestone_new| + milestone_new.set_title(@title) + milestone_new.set_description(@description) + milestone_new.create_new_milestone + end + end + end + end +end diff --git a/qa/qa/resource/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb new file mode 100644 index 00000000000..c9fafe3419f --- /dev/null +++ b/qa/qa/resource/repository/project_push.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +module QA + module Resource + module Repository + class ProjectPush < Repository::Push + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-code' + resource.description = 'Project with repository' + end + end + + def initialize + @file_name = 'file.txt' + @file_content = '# This is test project' + @commit_message = "This is a test commit" + @branch_name = 'master' + @new_branch = true + end + + def repository_http_uri + @repository_http_uri ||= begin + project.visit! + Page::Project::Show.act do + choose_repository_clone_http + repository_location.uri + end + end + end + + def repository_ssh_uri + @repository_ssh_uri ||= begin + project.visit! + Page::Project::Show.act do + choose_repository_clone_ssh + repository_location.uri + end + end + end + end + end + end +end diff --git a/qa/qa/resource/repository/push.rb b/qa/qa/resource/repository/push.rb new file mode 100644 index 00000000000..c14d97ff7fb --- /dev/null +++ b/qa/qa/resource/repository/push.rb @@ -0,0 +1,93 @@ +# frozen_string_literal: true + +require 'pathname' + +module QA + module Resource + module Repository + class Push < Base + attr_accessor :file_name, :file_content, :commit_message, + :branch_name, :new_branch, :output, :repository_http_uri, + :repository_ssh_uri, :ssh_key, :user + + attr_writer :remote_branch + + def initialize + @file_name = 'file.txt' + @file_content = '# This is test file' + @commit_message = "This is a test commit" + @branch_name = 'master' + @new_branch = true + @repository_http_uri = "" + @ssh_key = nil + end + + def remote_branch + @remote_branch ||= branch_name + end + + def directory=(dir) + raise "Must set directory as a Pathname" unless dir.is_a?(Pathname) + + @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 + repository.uri = repository_ssh_uri + repository.use_ssh_key(ssh_key) + else + repository.uri = repository_http_uri + repository.use_default_credentials unless user + end + + username = 'GitLab QA' + email = 'root@gitlab.com' + + if user + repository.username = user.username + repository.password = user.password + username = user.name + email = user.email + end + + repository.clone + repository.configure_identity(username, email) + + if new_branch + repository.checkout_new_branch(branch_name) + else + repository.checkout(branch_name) + end + + if @directory + @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 + + repository.commit(commit_message) + @output = repository.push_changes("#{branch_name}:#{remote_branch}") + + repository.delete_ssh_key + end + end + end + end + end +end diff --git a/qa/qa/resource/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb new file mode 100644 index 00000000000..f1c39d507fe --- /dev/null +++ b/qa/qa/resource/repository/wiki_push.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +module QA + module Resource + module Repository + class WikiPush < Repository::Push + attribute :wiki do + Wiki.fabricate! do |resource| + resource.title = 'Home' + resource.content = '# My First Wiki Content' + resource.message = 'Update home' + end + end + + def initialize + @file_name = 'Home.md' + @file_content = '# Welcome to My Wiki' + @commit_message = 'Updating Home Page' + @branch_name = 'master' + @new_branch = false + end + + def repository_http_uri + @repository_http_uri ||= begin + wiki.visit! + Page::Project::Wiki::Show.act do + go_to_clone_repository + choose_repository_clone_http + repository_location.uri + end + end + end + end + end + end +end diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb new file mode 100644 index 00000000000..08ae3f22117 --- /dev/null +++ b/qa/qa/resource/runner.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class Runner < Base + attr_writer :name, :tags, :image + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-with-ci-cd' + resource.description = 'Project with CI/CD Pipelines' + end + end + + def name + @name || "qa-runner-#{SecureRandom.hex(4)}" + end + + def tags + @tags || %w[qa e2e] + end + + def image + @image || 'gitlab/gitlab-runner:alpine' + end + + def fabricate! + project.visit! + + Page::Project::Menu.perform(&:click_ci_cd_settings) + + Service::Runner.new(name).tap do |runner| + Page::Project::Settings::CICD.perform do |settings| + settings.expand_runners_settings do |runners| + runner.pull + runner.token = runners.registration_token + runner.address = runners.coordinator_address + runner.tags = tags + runner.image = image + runner.register! + end + end + end + end + end + end +end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb new file mode 100644 index 00000000000..41ce857a8b8 --- /dev/null +++ b/qa/qa/resource/sandbox.rb @@ -0,0 +1,60 @@ +# frozen_string_literal: true + +module QA + module Resource + ## + # Ensure we're in our sandbox namespace, either by navigating to it or by + # creating it if it doesn't yet exist. + # + class Sandbox < Base + attr_reader :path + + attribute :id + + def initialize + @path = Runtime::Namespace.sandbox_name + end + + def fabricate! + Page::Main::Menu.perform(&:go_to_groups) + + Page::Dashboard::Groups.perform do |page| + 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(path) + group.set_description('GitLab QA Sandbox Group') + group.set_visibility('Public') + group.create + end + 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/resource/settings/hashed_storage.rb b/qa/qa/resource/settings/hashed_storage.rb new file mode 100644 index 00000000000..40c06768ffe --- /dev/null +++ b/qa/qa/resource/settings/hashed_storage.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Resource + module Settings + class HashedStorage < Base + def fabricate!(*traits) + raise ArgumentError unless traits.include?(:enabled) + + Page::Main::Login.perform(&:sign_in_using_credentials) + Page::Main::Menu.perform(&:go_to_admin_area) + Page::Admin::Menu.perform(&:go_to_repository_settings) + + Page::Admin::Settings::Repository.perform do |setting| + setting.expand_repository_storage do |page| + page.enable_hashed_storage + page.save_settings + end + end + + QA::Page::Main::Menu.perform(&:sign_out) + end + end + end + end +end diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb new file mode 100644 index 00000000000..c6c97c8532f --- /dev/null +++ b/qa/qa/resource/ssh_key.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Resource + class SSHKey < Base + extend Forwardable + + attr_accessor :title + + def_delegators :key, :private_key, :public_key, :fingerprint + + def key + @key ||= Runtime::Key::RSA.new + end + + def fabricate! + 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) + end + end + end + end +end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb new file mode 100644 index 00000000000..16f0b311fa9 --- /dev/null +++ b/qa/qa/resource/user.rb @@ -0,0 +1,92 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class User < Base + attr_reader :unique_id + attr_writer :username, :password + + def initialize + @unique_id = SecureRandom.hex(8) + end + + def username + @username ||= "qa-user-#{unique_id}" + end + + def password + @password ||= 'password' + end + + def name + @name ||= username + end + + def email + @email ||= "#{username}@example.com" + end + + def credentials_given? + defined?(@username) && defined?(@password) + end + + def fabricate! + # Don't try to log-out if we're not logged-in + if Page::Main::Menu.perform { |p| p.has_personal_area?(wait: 0) } + Page::Main::Menu.perform { |main| main.sign_out } + end + + if credentials_given? + Page::Main::Login.perform do |login| + login.sign_in_using_credentials(self) + end + else + Page::Main::Login.perform do |login| + login.switch_to_register_tab + end + Page::Main::SignUp.perform do |signup| + signup.sign_up!(self) + end + end + end + + def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError + super + end + + def api_get_path + "/users/#{fetch_id(username)}" + end + + def api_post_path + '/users' + end + + def api_post_body + { + email: email, + password: password, + username: username, + name: name, + skip_confirmation: true + } + end + + private + + def fetch_id(username) + users = parse_body(api_get_from("/users?username=#{username}")) + + unless users.size == 1 && users.first[:username] == username + raise ResourceNotFoundError, "Expected one user with username #{username} but found: `#{users}`." + end + + users.first[:id] + end + end + end +end diff --git a/qa/qa/resource/wiki.rb b/qa/qa/resource/wiki.rb new file mode 100644 index 00000000000..e942e9718a0 --- /dev/null +++ b/qa/qa/resource/wiki.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + module Resource + class Wiki < Base + attr_accessor :title, :content, :message + + attribute :project do + Project.fabricate! do |resource| + resource.name = 'project-for-wikis' + resource.description = 'project for adding wikis' + end + end + + def fabricate! + 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 + end +end diff --git a/qa/qa/runtime/api/client.rb b/qa/qa/runtime/api/client.rb index 0545b500e4c..aff84c89f0e 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -32,7 +32,7 @@ module QA def do_create_personal_access_token Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::PersonalAccessToken.fabricate!.access_token + Resource::PersonalAccessToken.fabricate!.access_token end end end 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 4f960ee26a9..7d5fc3c4b65 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 @@ -5,7 +5,7 @@ module QA it 'user registers and logs in' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Factory::Resource::User.fabricate_via_browser_ui! + Resource::User.fabricate! # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. 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 2cd5bf01c1f..bef89d5be24 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 @@ -7,9 +7,9 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - user = Factory::Resource::User.fabricate! + user = Resource::User.fabricate! - project = Factory::Resource::Project.fabricate! do |resource| + project = Resource::Project.fabricate! do |resource| resource.name = 'add-member-project' end project.visit! 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 a242f2158da..6632c2977ef 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - created_project = Factory::Resource::Project.fabricate_via_browser_ui! do |project| + created_project = Resource::Project.fabricate_via_browser_ui! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' 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 a99b0522e73..3ce48de2c25 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 @@ -4,7 +4,7 @@ module QA context 'Manage', :orchestrated, :github do describe 'Project import from GitHub' do let(:imported_project) do - Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| + Resource::ProjectImportedFromGithub.fabricate! do |project| project.name = 'imported-project' project.personal_access_token = Runtime::Env.github_access_token project.github_repository_path = 'gitlab-qa/test-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 768d40f3acf..275de3d332c 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 @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' 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 e67561b3a39..f5002c8032f 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 @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb index 24877d937d2..83603f1cda7 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Issue.fabricate! do |issue| + Resource::Issue.fabricate! do |issue| issue.title = issue_title end 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 037ff5efbd4..d33947f41da 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 @@ -7,22 +7,22 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request-and-milestone' end - current_milestone = Factory::Resource::ProjectMilestone.fabricate! do |milestone| + current_milestone = Resource::ProjectMilestone.fabricate! do |milestone| milestone.title = 'unique-milestone' milestone.project = current_project end - new_label = Factory::Resource::Label.fabricate! do |label| + new_label = 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| + 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 @@ -49,11 +49,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - current_project = Factory::Resource::Project.fabricate! do |project| + current_project = Resource::Project.fabricate! do |project| project.name = 'project-with-merge-request' end - Factory::Resource::MergeRequest.fabricate! do |merge_request| + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request' merge_request.description = 'Great feature' merge_request.project = current_project 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 058af8aebdd..6dcd74471fe 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 @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - merge_request = Factory::Resource::MergeRequestFromFork.fabricate! do |merge_request| + merge_request = Resource::MergeRequestFromFork.fabricate! do |merge_request| merge_request.fork_branch = 'feature-branch' end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/rebase_merge_request_spec.rb index 3bcf086d332..e2d639fd150 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 @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "only-fast-forward" end project.visit! @@ -15,12 +15,12 @@ module QA Page::Project::Menu.act { go_to_settings } Page::Project::Settings::MergeRequest.act { enable_ff_only } - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Needs rebasing' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = "other.txt" push.file_content = "New file added!" 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 724c48cd125..6ff7360c413 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 @@ -7,16 +7,16 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = "squash-before-merge" end - merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request| + merge_request = Resource::MergeRequest.fabricate! do |merge_request| merge_request.project = project merge_request.title = 'Squashing commits' end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.commit_message = 'to be squashed' push.branch_name = merge_request.source_branch 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 7705e12b95e..297485dd81e 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 @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Files view' end 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 df70b9608d9..94be66782c6 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 @@ -9,7 +9,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end 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 b18dee53cbc..6a0add56fe0 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb @@ -14,7 +14,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |scenario| + project = Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end 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 f65a1569fb0..46346d1b984 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 @@ -12,7 +12,7 @@ module QA file_content = 'QA Test - File content' commit_message_for_create = 'QA Test - Create new file' - Factory::Resource::File.fabricate! do |file| + Resource::File.fabricate! do |file| file.name = file_name file.content = file_content file.commit_message = commit_message_for_create diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb index 8e4210482a2..a63b7dce8d6 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -7,14 +7,14 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.perform(&:sign_in_using_credentials) - access_token = Factory::Resource::PersonalAccessToken.fabricate!.access_token + access_token = Resource::PersonalAccessToken.fabricate!.access_token - user = Factory::Resource::User.new.tap do |user| + user = Resource::User.new.tap do |user| user.username = Runtime::User.username user.password = access_token end - push = Factory::Repository::ProjectPush.fabricate! do |push| + push = Resource::Repository::ProjectPush.fabricate! do |push| push.user = user push.file_name = 'README.md' push.file_content = '# This is a test project' 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 2f63a07e0c3..92f596a44d9 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 @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.file_name = 'README.md' push.file_content = '# This is a test project' push.commit_message = 'Add README.md' 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 ac71cf52b6f..73a3dc14a65 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 @@ -6,7 +6,7 @@ module QA let(:branch_name) { 'protected-branch' } let(:commit_message) { 'Protected push commit message' } let(:project) do - Factory::Resource::Project.fabricate! do |resource| + Resource::Project.fabricate! do |resource| resource.name = 'protected-branch-project' end end @@ -47,7 +47,7 @@ module QA end def create_protected_branch(allow_to_push:) - Factory::Resource::Branch.fabricate! do |resource| + Resource::Branch.fabricate! do |resource| resource.branch_name = branch_name resource.project = project resource.allow_to_push = allow_to_push @@ -56,7 +56,7 @@ module QA end def push_new_file(branch) - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = project resource.file_name = 'new_file.md' resource.file_content = '# This is a new file' 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 36068ffba69..9c764424129 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 @@ -12,11 +12,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - key = Factory::Resource::SSHKey.fabricate! do |resource| + key = Resource::SSHKey.fabricate! do |resource| resource.title = key_title end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.ssh_key = key push.file_name = 'README.md' push.file_content = '# Test Use SSH Key' 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 07dbf39a8a3..e7374377104 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 @@ -13,7 +13,7 @@ module QA before(:all) do login - @project = Factory::Resource::Project.fabricate! do |project| + @project = Resource::Project.fabricate! do |project| project.name = 'file-template-project' project.description = 'Add file templates via the Web IDE' end 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 4126fd9fd3e..210271705d9 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 @@ -18,7 +18,7 @@ module QA end it 'user creates, edits, clones, and pushes to the wiki' do - wiki = Factory::Resource::Wiki.fabricate! do |resource| + wiki = Resource::Wiki.fabricate! do |resource| resource.title = 'Home' resource.content = '# My First Wiki Content' resource.message = 'Update home' @@ -34,7 +34,7 @@ module QA validate_content('My Second Wiki Content') - Factory::Repository::WikiPush.fabricate! do |push| + Resource::Repository::WikiPush.fabricate! do |push| push.wiki = wiki push.file_name = 'Home.md' push.file_content = '# My Third Wiki Content' diff --git a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 58b272adcf1..0837b720df1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -7,7 +7,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' resource.value = 'some CI variable' end 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 d66bcce879b..25cbe41c684 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 @@ -13,18 +13,18 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |project| + project = Resource::Project.fabricate! do |project| project.name = 'project-with-pipelines' project.description = 'Project with CI/CD Pipelines.' end - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.project = project runner.name = executor runner.tags = %w[qa test] end - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.file_name = '.gitlab-ci.yml' push.commit_message = 'Add .gitlab-ci.yml' 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 5d9aa00582f..3af7db751e7 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 @@ -13,7 +13,7 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Runner.fabricate! do |runner| + Resource::Runner.fabricate! do |runner| runner.name = executor end 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 64b98da8bf5..84757f25379 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 @@ -11,7 +11,7 @@ module QA deploy_key_title = 'deploy key title' deploy_key_value = key.public_key - deploy_key = Factory::Resource::DeployKey.fabricate! do |resource| + deploy_key = Resource::DeployKey.fabricate! do |resource| resource.title = deploy_key_title resource.key = deploy_key_value end 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 604641e54b8..e2320c92343 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 @@ -15,13 +15,13 @@ module QA @runner_name = "qa-runner-#{Time.now.to_i}" - @project = Factory::Resource::Project.fabricate! do |resource| + @project = Resource::Project.fabricate! do |resource| resource.name = 'deploy-key-clone-project' end @repository_location = @project.repository_ssh_location - Factory::Resource::Runner.fabricate! do |resource| + Resource::Runner.fabricate! do |resource| resource.project = @project resource.name = @runner_name resource.tags = %w[qa docker] @@ -47,7 +47,7 @@ module QA login - Factory::Resource::DeployKey.fabricate! do |resource| + Resource::DeployKey.fabricate! do |resource| resource.project = @project resource.title = "deploy key #{key.name}(#{key.bits})" resource.key = key.public_key @@ -55,7 +55,7 @@ module QA deploy_key_name = "DEPLOY_KEY_#{key.name}_#{key.bits}" - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = @project resource.key = deploy_key_name resource.value = key.private_key @@ -78,7 +78,7 @@ module QA - docker YAML - Factory::Repository::ProjectPush.fabricate! do |resource| + Resource::Repository::ProjectPush.fabricate! do |resource| resource.project = @project resource.file_name = '.gitlab-ci.yml' resource.commit_message = 'Add .gitlab-ci.yml' 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 263ba6a6800..9f34e4218c1 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 @@ -10,7 +10,7 @@ module QA deploy_token_name = 'deploy token name' deploy_token_expires_at = Date.today + 7 # 1 Week from now - deploy_token = Factory::Resource::DeployToken.fabricate! do |resource| + deploy_token = Resource::DeployToken.fabricate! do |resource| resource.name = deploy_token_name resource.expires_at = deploy_token_expires_at end 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 c2fce1e7df1..30ec0665973 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 @@ -15,21 +15,21 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |p| + project = Resource::Project.fabricate! do |p| p.name = 'project-with-autodevops' p.description = 'Project with Auto Devops' end # Disable code_quality check in Auto DevOps pipeline as it takes # too long and times out the test - Factory::Resource::CiVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.project = project resource.key = 'CODE_QUALITY_DISABLED' resource.value = '1' end # Create Auto Devops compatible repo - Factory::Repository::ProjectPush.fabricate! do |push| + Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.directory = Pathname .new(__dir__) @@ -41,7 +41,7 @@ module QA # Create and connect K8s cluster @cluster = Service::KubernetesCluster.new(rbac: rbac).create! - kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| + kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster| cluster.project = project cluster.cluster = @cluster cluster.install_helm_tiller = true diff --git a/qa/spec/factory/api_fabricator_spec.rb b/qa/spec/factory/api_fabricator_spec.rb deleted file mode 100644 index e5fbc064911..00000000000 --- a/qa/spec/factory/api_fabricator_spec.rb +++ /dev/null @@ -1,161 +0,0 @@ -# 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 deleted file mode 100644 index 990eba76460..00000000000 --- a/qa/spec/factory/base_spec.rb +++ /dev/null @@ -1,246 +0,0 @@ -# frozen_string_literal: true - -describe QA::Factory::Base do - include Support::StubENV - - let(:factory) { spy('factory') } - let(:location) { 'http://location' } - - shared_context 'fabrication context' do - subject do - Class.new(described_class) do - def self.name - 'MyFactory' - end - end - end - - before do - allow(subject).to receive(:current_url).and_return(location) - allow(subject).to receive(:new).and_return(factory) - end - end - - 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(location) - - subject.public_send(fabrication_method_called, factory: factory) do |factory| - factory.something! - end - end - - it 'does not log the factory and build method when QA_DEBUG=false' do - stub_env('QA_DEBUG', 'false') - expect(factory).to receive(fabrication_method_used).and_return(location) - - expect { subject.public_send(fabrication_method_called, 'something', factory: factory) } - .not_to output.to_stdout - end - end - - describe '.fabricate!' do - context 'when factory does not support fabrication via the API' do - before do - expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError) - end - - it 'calls .fabricate_via_browser_ui!' do - expect(described_class).to receive(:fabricate_via_browser_ui!) - - described_class.fabricate! - end - end - - context 'when factory supports fabrication via the API' do - it 'calls .fabricate_via_browser_ui!' do - expect(described_class).to receive(:fabricate_via_api!) - - described_class.fabricate! - end - end - end - - describe '.fabricate_via_api!' do - include_context 'fabrication context' - - it_behaves_like 'fabrication method', :fabricate_via_api! - - it 'instantiates the factory, calls factory method returns the resource' do - expect(factory).to receive(:fabricate_via_api!).and_return(location) - - result = subject.fabricate_via_api!(factory: factory, parents: []) - - expect(result).to eq(factory) - end - - 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(location) - - expect { subject.fabricate_via_api!('something', factory: factory, parents: []) } - .to output(/==> Built a MyFactory via api in [\d\.\-e]+ seconds+/) - .to_stdout - end - end - - describe '.fabricate_via_browser_ui!' do - include_context 'fabrication context' - - it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! - - it 'instantiates the factory and calls factory method' do - subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) - - expect(factory).to have_received(:fabricate!).with('something') - end - - it 'returns fabrication resource' do - result = subject.fabricate_via_browser_ui!('something', factory: factory, parents: []) - - expect(result).to eq(factory) - 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 in [\d\.\-e]+ seconds+/) - .to_stdout - end - end - - shared_context 'simple factory' do - subject do - 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 - - let(:factory) { subject.new } - end - - describe '.attribute' do - include_context 'simple factory' - - it 'appends new attribute' do - expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) - end - - context 'when the attribute is populated via a block' do - it 'returns value from the block' do - result = subject.fabricate!(factory: factory) - - expect(result).to be_a(described_class) - expect(result.test).to eq('block') - end - end - - context 'when the attribute is populated via the api' do - let(:api_resource) { { no_block: 'api' } } - - before do - expect(factory).to receive(:api_resource).and_return(api_resource) - end - - it 'returns value from api' do - result = subject.fabricate!(factory: factory) - - expect(result).to be_a(described_class) - expect(result.no_block).to eq('api') - end - - context 'when the attribute also has a block' do - let(:api_resource) { { test: 'api_with_block' } } - - before do - allow(QA::Runtime::Logger).to receive(:info) - end - - it 'returns value from api and emits an INFO log entry' do - result = subject.fabricate!(factory: factory) - - expect(result).to be_a(described_class) - expect(result.test).to eq('api_with_block') - expect(QA::Runtime::Logger) - .to have_received(:info).with(/api_with_block/) - end - end - end - - context 'when the attribute is populated via direct assignment' do - before do - factory.test = 'value' - end - - it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) - - expect(result).to be_a(described_class) - 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 - - it 'returns value from the assignment' do - result = subject.fabricate!(factory: factory) - - expect(result).to be_a(described_class) - expect(result.test).to eq('value') - end - end - end - - context 'when the 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 no_block of #{factory.class.name}.") - end - end - end - - describe '#web_url' do - include_context 'simple factory' - - it 'sets #web_url to #current_url after fabrication' do - subject.fabricate!(factory: factory) - - expect(factory.web_url).to eq(subject.current_url) - end - end - - describe '#visit!' do - include_context 'simple factory' - - before do - allow(factory).to receive(:visit) - end - - it '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/repository/push_spec.rb b/qa/spec/factory/repository/push_spec.rb deleted file mode 100644 index 2eb6c008248..00000000000 --- a/qa/spec/factory/repository/push_spec.rb +++ /dev/null @@ -1,26 +0,0 @@ -# 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/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb index 2f6c59b3e69..820c506b715 100644 --- a/qa/spec/factory/resource/user_spec.rb +++ b/qa/spec/factory/resource/user_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -describe QA::Factory::Resource::User do +describe QA::Resource::User do describe "#fabricate_via_api!" do Response = Struct.new(:code, :body) diff --git a/qa/spec/resource/api_fabricator_spec.rb b/qa/spec/resource/api_fabricator_spec.rb new file mode 100644 index 00000000000..a5ed4422f6e --- /dev/null +++ b/qa/spec/resource/api_fabricator_spec.rb @@ -0,0 +1,161 @@ +# frozen_string_literal: true + +describe QA::Resource::ApiFabricator do + let(:resource_without_api_support) do + Class.new do + def self.name + 'FooBarResource' + end + end + end + + let(:resource_with_api_support) do + Class.new do + def self.name + 'FooBarResource' + 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 { resource.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 resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } + + it 'returns false' do + expect(subject).not_to be_api_support + end + end + + context 'when resource supports fabrication via the API' do + let(:resource) { resource_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 resource does not support fabrication via the API' do + let(:resource) { resource_without_api_support } + + it 'raises a NotImplementedError exception' do + expect { subject.fabricate_via_api! }.to raise_error(NotImplementedError, "Resource FooBarResource does not support fabrication via the API!") + end + end + + context 'when resource supports fabrication via the API' do + let(:resource) { resource_with_api_support } + let(:api_request) { spy('Runtime::API::Request') } + let(:resource_web_url) { 'http://example.org/api/v4/foo' } + let(:response) { { id: 1, name: 'John Doe', web_url: resource_web_url } } + let(:raw_post) { double('Raw POST response', code: 201, body: response.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(response) + 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 FooBarResource using the API failed (400) with `#{raw_post}`.") + expect(subject.api_resource).to be_nil + end + end + end + + context '#transform_api_resource' do + let(:resource) do + Class.new do + def self.name + 'FooBarResource' + 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(:response) { { 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(response).and_return(transformed_resource) + + subject.fabricate_via_api! + end + end + end + end +end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb new file mode 100644 index 00000000000..dc9e16792d3 --- /dev/null +++ b/qa/spec/resource/base_spec.rb @@ -0,0 +1,246 @@ +# frozen_string_literal: true + +describe QA::Resource::Base do + include Support::StubENV + + let(:resource) { spy('resource') } + let(:location) { 'http://location' } + + shared_context 'fabrication context' do + subject do + Class.new(described_class) do + def self.name + 'MyResource' + end + end + end + + before do + allow(subject).to receive(:current_url).and_return(location) + allow(subject).to receive(:new).and_return(resource) + end + end + + shared_examples 'fabrication method' do |fabrication_method_called, actual_fabrication_method = nil| + let(:fabrication_method_used) { actual_fabrication_method || fabrication_method_called } + + it 'yields resource before calling resource method' do + expect(resource).to receive(:something!).ordered + expect(resource).to receive(fabrication_method_used).ordered.and_return(location) + + subject.public_send(fabrication_method_called, resource: resource) do |resource| + resource.something! + end + end + + it 'does not log the resource and build method when QA_DEBUG=false' do + stub_env('QA_DEBUG', 'false') + expect(resource).to receive(fabrication_method_used).and_return(location) + + expect { subject.public_send(fabrication_method_called, 'something', resource: resource) } + .not_to output.to_stdout + end + end + + describe '.fabricate!' do + context 'when resource does not support fabrication via the API' do + before do + expect(described_class).to receive(:fabricate_via_api!).and_raise(NotImplementedError) + end + + it 'calls .fabricate_via_browser_ui!' do + expect(described_class).to receive(:fabricate_via_browser_ui!) + + described_class.fabricate! + end + end + + context 'when resource supports fabrication via the API' do + it 'calls .fabricate_via_browser_ui!' do + expect(described_class).to receive(:fabricate_via_api!) + + described_class.fabricate! + end + end + end + + describe '.fabricate_via_api!' do + include_context 'fabrication context' + + it_behaves_like 'fabrication method', :fabricate_via_api! + + it 'instantiates the resource, calls resource method returns the resource' do + expect(resource).to receive(:fabricate_via_api!).and_return(location) + + result = subject.fabricate_via_api!(resource: resource, parents: []) + + expect(result).to eq(resource) + end + + it 'logs the resource and build method when QA_DEBUG=true' do + stub_env('QA_DEBUG', 'true') + expect(resource).to receive(:fabricate_via_api!).and_return(location) + + expect { subject.fabricate_via_api!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via api in [\d\.\-e]+ seconds+/) + .to_stdout + end + end + + describe '.fabricate_via_browser_ui!' do + include_context 'fabrication context' + + it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! + + it 'instantiates the resource and calls resource method' do + subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) + + expect(resource).to have_received(:fabricate!).with('something') + end + + it 'returns fabrication resource' do + result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) + + expect(result).to eq(resource) + end + + it 'logs the resource and build method when QA_DEBUG=true' do + stub_env('QA_DEBUG', 'true') + + expect { subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) } + .to output(/==> Built a MyResource via browser_ui in [\d\.\-e]+ seconds+/) + .to_stdout + end + end + + shared_context 'simple resource' do + subject do + Class.new(QA::Resource::Base) do + attribute :test do + 'block' + end + + attribute :no_block + + def fabricate! + 'any' + end + + def self.current_url + 'http://stub' + end + end + end + + let(:resource) { subject.new } + end + + describe '.attribute' do + include_context 'simple resource' + + it 'appends new attribute' do + expect(subject.attributes_names).to eq([:no_block, :test, :web_url]) + end + + context 'when the attribute is populated via a block' do + it 'returns value from the block' do + result = subject.fabricate!(resource: resource) + + expect(result).to be_a(described_class) + expect(result.test).to eq('block') + end + end + + context 'when the attribute is populated via the api' do + let(:api_resource) { { no_block: 'api' } } + + before do + expect(resource).to receive(:api_resource).and_return(api_resource) + end + + it 'returns value from api' do + result = subject.fabricate!(resource: resource) + + expect(result).to be_a(described_class) + expect(result.no_block).to eq('api') + end + + context 'when the attribute also has a block' do + let(:api_resource) { { test: 'api_with_block' } } + + before do + allow(QA::Runtime::Logger).to receive(:info) + end + + it 'returns value from api and emits an INFO log entry' do + result = subject.fabricate!(resource: resource) + + expect(result).to be_a(described_class) + expect(result.test).to eq('api_with_block') + expect(QA::Runtime::Logger) + .to have_received(:info).with(/api_with_block/) + end + end + end + + context 'when the attribute is populated via direct assignment' do + before do + resource.test = 'value' + end + + it 'returns value from the assignment' do + result = subject.fabricate!(resource: resource) + + expect(result).to be_a(described_class) + expect(result.test).to eq('value') + end + + context 'when the api also has such response' do + before do + allow(resource).to receive(:api_resource).and_return({ test: 'api' }) + end + + it 'returns value from the assignment' do + result = subject.fabricate!(resource: resource) + + expect(result).to be_a(described_class) + expect(result.test).to eq('value') + end + end + end + + context 'when the attribute has no value' do + it 'raises an error because no values could be found' do + result = subject.fabricate!(resource: resource) + + expect { result.no_block } + .to raise_error(described_class::NoValueError, "No value was computed for no_block of #{resource.class.name}.") + end + end + end + + describe '#web_url' do + include_context 'simple resource' + + it 'sets #web_url to #current_url after fabrication' do + subject.fabricate!(resource: resource) + + expect(resource.web_url).to eq(subject.current_url) + end + end + + describe '#visit!' do + include_context 'simple resource' + + before do + allow(resource).to receive(:visit) + end + + it 'calls #visit with the underlying #web_url' do + resource.web_url = subject.current_url + resource.visit! + + expect(resource).to have_received(:visit).with(subject.current_url) + end + end +end diff --git a/qa/spec/resource/repository/push_spec.rb b/qa/spec/resource/repository/push_spec.rb new file mode 100644 index 00000000000..bf3ebce0cfe --- /dev/null +++ b/qa/spec/resource/repository/push_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +describe QA::Resource::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 -- cgit v1.2.1