diff options
Diffstat (limited to 'qa')
196 files changed, 4850 insertions, 1821 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile index abf2184e1e2..9956ced0ef6 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -3,10 +3,20 @@ LABEL maintainer "Grzegorz Bizon <grzegorz@gitlab.com>" ENV DEBIAN_FRONTEND noninteractive ## +# Add support for stretch-backports +# +RUN echo "deb http://ftp.debian.org/debian stretch-backports main" >> /etc/apt/sources.list + +## # Update APT sources and install some dependencies # RUN sed -i "s/httpredir.debian.org/ftp.us.debian.org/" /etc/apt/sources.list -RUN apt-get update && apt-get install -y wget git unzip xvfb +RUN apt-get update && apt-get install -y wget unzip xvfb + +## +# Install some packages from backports +# +RUN apt-get -y -t stretch-backports install git git-lfs ## # Install Docker diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 8d28fcacc05..d61ecf8fbb5 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -56,7 +56,7 @@ GEM byebug (~> 9.1) pry (~> 0.10) public_suffix (3.0.1) - rack (2.0.3) + rack (2.0.6) rack-test (0.8.2) rack (>= 1.0, < 3) rake (12.3.0) @@ -103,4 +103,4 @@ DEPENDENCIES selenium-webdriver (~> 3.8.0) BUNDLED WITH - 1.16.4 + 1.17.1 diff --git a/qa/README.md b/qa/README.md index 746bd5cf94b..08ba59e117d 100644 --- a/qa/README.md +++ b/qa/README.md @@ -80,6 +80,15 @@ GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sa All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/what_tests_can_be_run.md#supported-environment-variables). +### Sending additional cookies + +The environment variable `QA_COOKIES` can be set to send additional cookies +on every request. This is necessary on gitlab.com to direct traffic to the +canary fleet. To do this set `QA_COOKIES="gitlab_canary=true"`. + +To set multiple cookies, separate them with the `;` character, for example: `QA_COOKIES="cookie1=value;cookie2=value2"` + + ### Building a Docker image to test Once you have made changes to the CE/EE repositories, you may want to build a @@ -16,6 +16,9 @@ module QA autoload :Browser, 'qa/runtime/browser' autoload :Env, 'qa/runtime/env' autoload :Address, 'qa/runtime/address' + autoload :Path, 'qa/runtime/path' + autoload :Fixtures, 'qa/runtime/fixtures' + autoload :Logger, 'qa/runtime/logger' module API autoload :Client, 'qa/runtime/api/client' @@ -33,41 +36,40 @@ module QA ## # GitLab QA fabrication mechanisms # - module Factory - autoload :Base, 'qa/factory/base' - autoload :Dependency, 'qa/factory/dependency' - autoload :Product, 'qa/factory/product' - - 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 :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 :Branch, 'qa/factory/resource/branch' - autoload :SecretVariable, 'qa/factory/resource/secret_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 @@ -94,7 +96,9 @@ module QA module Integration autoload :Github, 'qa/scenario/test/integration/github' - autoload :LDAP, 'qa/scenario/test/integration/ldap' + autoload :LDAPNoTLS, 'qa/scenario/test/integration/ldap_no_tls' + autoload :LDAPTLS, 'qa/scenario/test/integration/ldap_tls' + autoload :InstanceSAML, 'qa/scenario/test/integration/instance_saml' autoload :Kubernetes, 'qa/scenario/test/integration/kubernetes' autoload :Mattermost, 'qa/scenario/test/integration/mattermost' autoload :ObjectStorage, 'qa/scenario/test/integration/object_storage' @@ -120,6 +124,7 @@ module QA module Main autoload :Login, 'qa/page/main/login' + autoload :Menu, 'qa/page/main/menu' autoload :OAuth, 'qa/page/main/oauth' autoload :SignUp, 'qa/page/main/sign_up' end @@ -128,13 +133,6 @@ module QA autoload :Common, 'qa/page/settings/common' end - module Menu - autoload :Main, 'qa/page/menu/main' - autoload :Side, 'qa/page/menu/side' - autoload :Admin, 'qa/page/menu/admin' - autoload :Profile, 'qa/page/menu/profile' - end - module Dashboard autoload :Projects, 'qa/page/dashboard/projects' autoload :Groups, 'qa/page/dashboard/groups' @@ -158,6 +156,7 @@ module QA autoload :New, 'qa/page/project/new' autoload :Show, 'qa/page/project/show' autoload :Activity, 'qa/page/project/activity' + autoload :Menu, 'qa/page/project/menu' module Import autoload :Github, 'qa/page/project/import/github' @@ -179,10 +178,12 @@ module QA autoload :Repository, 'qa/page/project/settings/repository' autoload :CICD, 'qa/page/project/settings/ci_cd' autoload :DeployKeys, 'qa/page/project/settings/deploy_keys' + autoload :DeployTokens, 'qa/page/project/settings/deploy_tokens' autoload :ProtectedBranches, 'qa/page/project/settings/protected_branches' - autoload :SecretVariables, 'qa/page/project/settings/secret_variables' + autoload :CiVariables, 'qa/page/project/settings/ci_variables' autoload :Runners, 'qa/page/project/settings/runners' autoload :MergeRequest, 'qa/page/project/settings/merge_request' + autoload :Members, 'qa/page/project/settings/members' end module Issue @@ -201,6 +202,11 @@ module QA end module Operations + module Environments + autoload :Index, 'qa/page/project/operations/environments/index' + autoload :Show, 'qa/page/project/operations/environments/show' + end + module Kubernetes autoload :Index, 'qa/page/project/operations/kubernetes/index' autoload :Add, 'qa/page/project/operations/kubernetes/add' @@ -214,9 +220,14 @@ module QA autoload :New, 'qa/page/project/wiki/new' autoload :Show, 'qa/page/project/wiki/show' end + + module WebIDE + autoload :Edit, 'qa/page/project/web_ide/edit' + end end module Profile + autoload :Menu, 'qa/page/profile/menu' autoload :PersonalAccessTokens, 'qa/page/profile/personal_access_tokens' autoload :SSHKeys, 'qa/page/profile/ssh_keys' end @@ -229,15 +240,25 @@ module QA autoload :Banner, 'qa/page/layout/banner' end + module Label + autoload :New, 'qa/page/label/new' + autoload :Index, 'qa/page/label/index' + end + module MergeRequest autoload :New, 'qa/page/merge_request/new' autoload :Show, 'qa/page/merge_request/show' end module Admin + autoload :Menu, 'qa/page/admin/menu' + module Settings - autoload :RepositoryStorage, 'qa/page/admin/settings/repository_storage' - autoload :Main, 'qa/page/admin/settings/main' + autoload :Repository, 'qa/page/admin/settings/repository' + + module Component + autoload :RepositoryStorage, 'qa/page/admin/settings/component/repository_storage' + end end end @@ -254,6 +275,12 @@ module QA autoload :Dropzone, 'qa/page/component/dropzone' autoload :GroupsFilter, 'qa/page/component/groups_filter' autoload :Select2, 'qa/page/component/select2' + autoload :DropdownFilter, 'qa/page/component/dropdown_filter' + autoload :UsersSelect, 'qa/page/component/users_select' + + module Issuable + autoload :Common, 'qa/page/component/issuable/common' + end end end @@ -283,6 +310,27 @@ module QA autoload :Config, 'qa/specs/config' autoload :Runner, 'qa/specs/runner' end + + ## + # Classes that describe the structure of vendor/third party application pages + # + module Vendor + module SAMLIdp + module Page + autoload :Base, 'qa/vendor/saml_idp/page/base' + autoload :Login, 'qa/vendor/saml_idp/page/login' + end + end + end + + # Classes that provide support to other parts of the framework. + # + module Support + module Page + autoload :Logging, 'qa/support/page/logging' + end + autoload :Api, 'qa/support/api' + end end QA::Runtime::Release.extend_autoloads! diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb deleted file mode 100644 index 7a532ce534b..00000000000 --- a/qa/qa/factory/base.rb +++ /dev/null @@ -1,60 +0,0 @@ -require 'forwardable' - -module QA - module Factory - class Base - extend SingleForwardable - - def_delegators :evaluator, :dependency, :dependencies - def_delegators :evaluator, :product, :attributes - - def fabricate!(*_args) - raise NotImplementedError - end - - def self.fabricate!(*args) - new.tap do |factory| - yield factory if block_given? - - dependencies.each do |name, signature| - Factory::Dependency.new(name, factory, signature).build! - end - - factory.fabricate!(*args) - - break Factory::Product.populate!(factory) - end - end - - def self.evaluator - @evaluator ||= Factory::Base::DSL.new(self) - end - - class DSL - attr_reader :dependencies, :attributes - - def initialize(base) - @base = base - @dependencies = {} - @attributes = {} - end - - def dependency(factory, as:, &block) - as.tap do |name| - @base.class_eval { attr_accessor name } - - Dependency::Signature.new(factory, block).tap do |signature| - @dependencies.store(name, signature) - end - end - end - - def product(attribute, &block) - Product::Attribute.new(attribute, block).tap do |signature| - @attributes.store(attribute, signature) - end - end - end - end - end -end diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb deleted file mode 100644 index fc5dc82ce29..00000000000 --- a/qa/qa/factory/dependency.rb +++ /dev/null @@ -1,39 +0,0 @@ -module QA - module Factory - class Dependency - Signature = Struct.new(:factory, :block) - - def initialize(name, factory, signature) - @name = name - @factory = factory - @signature = signature - end - - def overridden? - !!@factory.public_send(@name) - end - - def build! - return if overridden? - - Builder.new(@signature, @factory).fabricate!.tap do |product| - @factory.public_send("#{@name}=", product) - end - end - - class Builder - def initialize(signature, caller_factory) - @factory = signature.factory - @block = signature.block - @caller_factory = caller_factory - end - - def fabricate! - @factory.fabricate! do |factory| - @block&.call(factory, @caller_factory) - end - end - end - end - end -end diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb deleted file mode 100644 index 996b7f14f61..00000000000 --- a/qa/qa/factory/product.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'capybara/dsl' - -module QA - module Factory - class Product - include Capybara::DSL - - Attribute = Struct.new(:name, :block) - - def initialize - @location = current_url - end - - def visit! - visit @location - end - - def self.populate!(factory) - new.tap do |product| - factory.class.attributes.each_value do |attribute| - product.instance_exec(factory, attribute.block) do |factory, block| - value = block.call(factory) - product.define_singleton_method(attribute.name) { value } - end - 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 60539992073..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 - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'protected-branch-project' - 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::Menu::Side.act do - click_repository_settings - end - - 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/deploy_key.rb b/qa/qa/factory/resource/deploy_key.rb deleted file mode 100644 index ea8a3ad687d..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 - - product :fingerprint do |resource| - Page::Project::Settings::Repository.act do - expand_deploy_keys do |key| - key_offset = key.key_titles.index do |title| - title.text == resource.title - end - - key.key_fingerprints[key_offset].text - end - end - end - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-to-deploy' - project.description = 'project for adding deploy key test' - end - - def fabricate! - project.visit! - - Page::Menu::Side.act do - click_repository_settings - end - - 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/file.rb b/qa/qa/factory/resource/file.rb deleted file mode 100644 index 2016d10ddae..00000000000 --- a/qa/qa/factory/resource/file.rb +++ /dev/null @@ -1,34 +0,0 @@ -module QA - module Factory - module Resource - class File < Factory::Base - attr_accessor :name, - :content, - :commit_message - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-new-file' - 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.act { go_to_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 01969c31438..00000000000 --- a/qa/qa/factory/resource/fork.rb +++ /dev/null @@ -1,29 +0,0 @@ -module QA - module Factory - module Resource - class Fork < Factory::Base - dependency Factory::Repository::ProjectPush, as: :push - - dependency Factory::Resource::User, as: :user do |user| - if Runtime::Env.forker? - user.username = Runtime::Env.forker_username - user.password = Runtime::Env.forker_password - end - end - - product(:user) { |factory| factory.user } - - def fabricate! - push.project.visit! - Page::Project::Show.act { fork_project } - - Page::Project::Fork::New.perform do |fork_new| - fork_new.choose_namespace(user.name) - end - - Page::Layout::Banner.act { has_notice?('The project was successfully forked.') } - 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 033fc48c08f..00000000000 --- a/qa/qa/factory/resource/group.rb +++ /dev/null @@ -1,41 +0,0 @@ -module QA - module Factory - module Resource - class Group < Factory::Base - attr_accessor :path, :description - - dependency Factory::Resource::Sandbox, as: :sandbox - - 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 - 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 95f48e20b3e..00000000000 --- a/qa/qa/factory/resource/issue.rb +++ /dev/null @@ -1,32 +0,0 @@ -module QA - module Factory - module Resource - class Issue < Factory::Base - attr_writer :title, :description, :project - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-for-issues' - project.description = 'project for adding issues' - end - - product :title do - Page::Project::Issue::Show.act { issue_title } - end - - def fabricate! - project.visit! - - Page::Project::Show.act do - go_to_new_issue - end - - 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 94d7df7128b..00000000000 --- a/qa/qa/factory/resource/kubernetes_cluster.rb +++ /dev/null @@ -1,59 +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 - - product :ingress_ip do - Page::Project::Operations::Kubernetes::Show.perform do |page| - page.ingress_ip - end - end - - def fabricate! - @project.visit! - - Page::Menu::Side.act { click_operations_kubernetes } - - Page::Project::Operations::Kubernetes::Index.perform do |page| - page.add_kubernetes_cluster - end - - Page::Project::Operations::Kubernetes::Add.perform do |page| - page.add_existing_cluster - end - - 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.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/merge_request.rb b/qa/qa/factory/resource/merge_request.rb deleted file mode 100644 index ddb62bd0a68..00000000000 --- a/qa/qa/factory/resource/merge_request.rb +++ /dev/null @@ -1,65 +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 - - product :project do |factory| - factory.project - end - - product :source_branch do |factory| - factory.source_branch - end - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-merge-request' - end - - dependency Factory::Repository::ProjectPush, as: :target do |push, factory| - factory.project.visit! - push.project = factory.project - push.branch_name = 'master' - push.remote_branch = factory.target_branch - end - - dependency Factory::Repository::ProjectPush, as: :source do |push, factory| - push.project = factory.project - push.branch_name = factory.target_branch - push.remote_branch = factory.source_branch - push.file_name = "added_file.txt" - push.file_content = "File Added" - 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! - project.visit! - Page::Project::Show.act { new_merge_request } - Page::MergeRequest::New.perform do |page| - page.fill_title(@title) - page.fill_description(@description) - page.choose_milestone(@milestone) if @milestone - 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 6caaf65f673..00000000000 --- a/qa/qa/factory/resource/merge_request_from_fork.rb +++ /dev/null @@ -1,24 +0,0 @@ -module QA - module Factory - module Resource - class MergeRequestFromFork < MergeRequest - attr_accessor :fork_branch - - dependency Factory::Resource::Fork, as: :fork - - dependency Factory::Repository::ProjectPush, as: :push do |push, factory| - push.project = factory.fork - push.branch_name = factory.fork_branch - push.file_name = 'file2.txt' - push.user = factory.fork.user - end - - def fabricate! - fork.visit! - Page::Project::Show.act { new_merge_request } - Page::MergeRequest::New.act { 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 514e3615d18..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 - - product :access_token do - Page::Profile::PersonalAccessTokens.act { created_access_token } - end - - def fabricate! - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { 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 90db26ab3ab..00000000000 --- a/qa/qa/factory/resource/project.rb +++ /dev/null @@ -1,54 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Project < Factory::Base - attr_writer :description - attr_reader :name - - dependency Factory::Resource::Group, as: :group - - product :name do |factory| - factory.name - end - - product :repository_ssh_location do - Page::Project::Show.act do - choose_repository_clone_ssh - repository_location - end - end - - product :repository_http_location do - Page::Project::Show.act do - choose_repository_clone_http - 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.act { 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 - 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 df2a3340d60..00000000000 --- a/qa/qa/factory/resource/project_imported_from_github.rb +++ /dev/null @@ -1,37 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class ProjectImportedFromGithub < Resource::Project - attr_writer :personal_access_token, :github_repository_path - - dependency Factory::Resource::Group, as: :group - - product :name do |factory| - factory.name - end - - def fabricate! - group.visit! - - Page::Group::Show.act { 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 47a5e74204f..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_accessor :description - attr_reader :title - - dependency Factory::Resource::Project, as: :project - - product(:title) { |factory| factory.title } - - def title=(title) - @title = "#{title}-#{SecureRandom.hex(4)}" - @description = 'A milestone' - end - - def fabricate! - project.visit! - - Page::Menu::Side.act do - click_issues - click_milestones - end - - Page::Project::Milestone::Index.act { 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 03b69eb1bdf..00000000000 --- a/qa/qa/factory/resource/runner.rb +++ /dev/null @@ -1,47 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class Runner < Factory::Base - attr_writer :name, :tags, :image - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-ci-cd' - project.description = 'Project with CI/CD Pipelines' - 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::Menu::Side.act { 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 4f6039f300f..00000000000 --- a/qa/qa/factory/resource/sandbox.rb +++ /dev/null @@ -1,34 +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 - def initialize - @name = Runtime::Namespace.sandbox_name - end - - def fabricate! - Page::Menu::Main.act { go_to_groups } - - Page::Dashboard::Groups.perform do |page| - if page.has_group?(@name) - page.go_to_group(@name) - else - page.go_to_new_group - - Page::Group::New.perform do |group| - group.set_path(@name) - group.set_description('GitLab QA Sandbox Group') - group.set_visibility('Public') - group.create - end - end - end - end - end - end - end -end diff --git a/qa/qa/factory/resource/secret_variable.rb b/qa/qa/factory/resource/secret_variable.rb deleted file mode 100644 index 12a830da116..00000000000 --- a/qa/qa/factory/resource/secret_variable.rb +++ /dev/null @@ -1,28 +0,0 @@ -module QA - module Factory - module Resource - class SecretVariable < Factory::Base - attr_accessor :key, :value - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-secret-variables' - project.description = 'project for adding secret variable test' - end - - def fabricate! - project.visit! - - Page::Menu::Side.act { click_ci_cd_settings } - - Page::Project::Settings::CICD.perform do |setting| - setting.expand_secret_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/ssh_key.rb b/qa/qa/factory/resource/ssh_key.rb deleted file mode 100644 index 6c872f32d16..00000000000 --- a/qa/qa/factory/resource/ssh_key.rb +++ /dev/null @@ -1,40 +0,0 @@ -# frozen_string_literal: true - -module QA - module Factory - module Resource - class SSHKey < Factory::Base - extend Forwardable - - attr_accessor :title - attr_reader :private_key, :public_key, :fingerprint - def_delegators :key, :private_key, :public_key, :fingerprint - - product :private_key do |factory| - factory.private_key - end - - product :title do |factory| - factory.title - end - - product :fingerprint do |factory| - factory.fingerprint - end - - def key - @key ||= Runtime::Key::RSA.new - end - - def fabricate! - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { 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 eac2a873bd5..00000000000 --- a/qa/qa/factory/resource/user.rb +++ /dev/null @@ -1,58 +0,0 @@ -require 'securerandom' - -module QA - module Factory - module Resource - class User < Factory::Base - attr_reader :unique_id - attr_writer :username, :password, :name, :email - - 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 - - product(:name) { |factory| factory.name } - product(:username) { |factory| factory.username } - product(:email) { |factory| factory.email } - product(:password) { |factory| factory.password } - - def fabricate! - Page::Menu::Main.perform { |main| main.sign_out } - - 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 - 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 cc200a512d5..00000000000 --- a/qa/qa/factory/resource/wiki.rb +++ /dev/null @@ -1,25 +0,0 @@ -module QA - module Factory - module Resource - class Wiki < Factory::Base - attr_accessor :title, :content, :message - - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-for-wikis' - project.description = 'project for adding wikis' - end - - def fabricate! - Page::Menu::Side.act { click_wiki } - Page::Project::Wiki::New.perform do |page| - page.go_to_create_first_page - page.set_title(@title) - page.set_content(@content) - page.set_message(@message) - page.create_new_page - 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 c69ebed3c6b..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.act { sign_in_using_credentials } - Page::Menu::Main.act { go_to_admin_area } - Page::Menu::Admin.act { go_to_settings } - - Page::Admin::Settings::Main.perform do |setting| - setting.expand_repository_storage do |page| - page.enable_hashed_storage - page.save_settings - end - end - - QA::Page::Menu::Main.act { sign_out } - end - end - end - end -end diff --git a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock index 09cf72c48ac..d44ccbb5e69 100644 --- a/qa/qa/fixtures/auto_devops_rack/Gemfile.lock +++ b/qa/qa/fixtures/auto_devops_rack/Gemfile.lock @@ -1,7 +1,7 @@ GEM remote: https://rubygems.org/ specs: - rack (2.0.4) + rack (2.0.6) rake (12.3.0) PLATFORMS @@ -12,4 +12,4 @@ DEPENDENCIES rake BUNDLED WITH - 1.16.1 + 1.17.1 diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 14cb8125fdb..7f959441dac 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -1,14 +1,24 @@ +# frozen_string_literal: true + require 'cgi' require 'uri' require 'open3' +require 'fileutils' +require 'tmpdir' module QA module Git class Repository include Scenario::Actable + attr_writer :password + attr_accessor :env_vars + def initialize - @ssh_cmd = "" + # We set HOME to the current working directory (which is a + # temporary directory created in .perform()) so the temporarily dropped + # .netrc can be utilised + self.env_vars = [%Q{HOME="#{File.dirname(netrc_file_path)}"}] end def self.perform(*args) @@ -21,36 +31,27 @@ module QA @uri = URI(address) end - def username=(name) - @username = name - @uri.user = name - end - - def password=(pass) - @password = pass - @uri.password = CGI.escape(pass).gsub('+', '%20') + def username=(username) + @username = username + @uri.user = username end def use_default_credentials - if ::QA::Runtime::User.ldap_user? - self.username = Runtime::User.ldap_username - self.password = Runtime::User.ldap_password - else - self.username = Runtime::User.username - self.password = Runtime::User.password - end + self.username, self.password = default_credentials + + add_credentials_to_netrc unless ssh_key_set? end def clone(opts = '') - run_and_redact_credentials(build_git_command("git clone #{opts} #{@uri} ./")) + run("git clone #{opts} #{uri} ./") end def checkout(branch_name) - `git checkout "#{branch_name}"` + run(%Q{git checkout "#{branch_name}"}) end def checkout_new_branch(branch_name) - `git checkout -b "#{branch_name}"` + run(%Q{git checkout -b "#{branch_name}"}) end def shallow_clone @@ -58,12 +59,10 @@ module QA end def configure_identity(name, email) - `git config user.name #{name}` - `git config user.email #{email}` - end + run(%Q{git config user.name #{name}}) + run(%Q{git config user.email #{email}}) - def configure_ssh_command(command) - @ssh_cmd = "GIT_SSH_COMMAND='#{command}'" + add_credentials_to_netrc end def commit_file(name, contents, message) @@ -74,54 +73,121 @@ module QA def add_file(name, contents) ::File.write(name, contents) - `git add #{name}` + run(%Q{git add #{name}}) end def commit(message) - `git commit -m "#{message}"` + run(%Q{git commit -m "#{message}"}) end def push_changes(branch = 'master') - output, _ = run_and_redact_credentials(build_git_command("git push #{@uri} #{branch}")) - - output + run("git push #{uri} #{branch}") end def commits - `git log --oneline`.split("\n") + run('git log --oneline').split("\n") end def use_ssh_key(key) @private_key_file = Tempfile.new("id_#{SecureRandom.hex(8)}") - File.binwrite(@private_key_file, key.private_key) - File.chmod(0700, @private_key_file) + File.binwrite(private_key_file, key.private_key) + File.chmod(0700, private_key_file) @known_hosts_file = Tempfile.new("known_hosts_#{SecureRandom.hex(8)}") keyscan_params = ['-H'] - keyscan_params << "-p #{@uri.port}" if @uri.port - keyscan_params << @uri.host - run_and_redact_credentials("ssh-keyscan #{keyscan_params.join(' ')} >> #{@known_hosts_file.path}") + keyscan_params << "-p #{uri.port}" if uri.port + keyscan_params << uri.host + run("ssh-keyscan #{keyscan_params.join(' ')} >> #{known_hosts_file.path}") - configure_ssh_command("ssh -i #{@private_key_file.path} -o UserKnownHostsFile=#{@known_hosts_file.path}") + self.env_vars << %Q{GIT_SSH_COMMAND="ssh -i #{private_key_file.path} -o UserKnownHostsFile=#{known_hosts_file.path}"} end def delete_ssh_key - return unless @private_key_file + return unless ssh_key_set? + + private_key_file.close(true) + known_hosts_file.close(true) + end + + def push_with_git_protocol(version, file_name, file_content, commit_message = 'Initial commit') + self.git_protocol = version + add_file(file_name, file_content) + commit(commit_message) + push_changes + + fetch_supported_git_protocol + end + + def git_protocol=(value) + raise ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2" unless %w[0 1 2].include?(value.to_s) - @private_key_file.close(true) - @known_hosts_file.close(true) + run("git config protocol.version #{value}") end - def build_git_command(command_str) - [@ssh_cmd, command_str].compact.join(' ') + def fetch_supported_git_protocol + # ls-remote is one command known to respond to Git protocol v2 so we use + # it to get output including the version reported via Git tracing + output = run("git ls-remote #{uri}", "GIT_TRACE_PACKET=1") + output[/git< version (\d+)/, 1] || 'unknown' end private - # Since the remote URL contains the credentials, and git occasionally - # outputs the URL. Note that stderr is redirected to stdout. - def run_and_redact_credentials(command) - Open3.capture2("#{command} 2>&1 | sed -E 's#://[^@]+@#://****@#g'") + attr_reader :uri, :username, :password, :known_hosts_file, :private_key_file + + def ssh_key_set? + !private_key_file.nil? + end + + def run(command_str, *extra_env) + command = [env_vars, *extra_env, command_str, '2>&1'].compact.join(' ') + Runtime::Logger.debug "Git: command=[#{command}]" + + output, _ = Open3.capture2(command) + output = output.chomp.gsub(/\s+$/, '') + Runtime::Logger.debug "Git: output=[#{output}]" + + output + end + + def default_credentials + if ::QA::Runtime::User.ldap_user? + [Runtime::User.ldap_username, Runtime::User.ldap_password] + else + [Runtime::User.username, Runtime::User.password] + end + end + + def tmp_netrc_directory + @tmp_netrc_directory ||= File.join(Dir.tmpdir, "qa-netrc-credentials", $$.to_s) + end + + def netrc_file_path + @netrc_file_path ||= File.join(tmp_netrc_directory, '.netrc') + end + + def netrc_content + "machine #{uri.host} login #{username} password #{password}" + end + + def netrc_already_contains_content? + File.exist?(netrc_file_path) && + File.readlines(netrc_file_path).grep(/^#{netrc_content}$/).any? + end + + def add_credentials_to_netrc + # Despite libcurl supporting a custom .netrc location through the + # CURLOPT_NETRC_FILE environment variable, git does not support it :( + # Info: https://curl.haxx.se/libcurl/c/CURLOPT_NETRC_FILE.html + # + # This will create a .netrc in the correct working directory, which is + # a temporary directory created in .perform() + # + return if netrc_already_contains_content? + + FileUtils.mkdir_p(tmp_netrc_directory) + File.open(netrc_file_path, 'a') { |file| file.puts(netrc_content) } + File.chmod(0600, netrc_file_path) end end end diff --git a/qa/qa/page/README.md b/qa/qa/page/README.md index 2dbc59846e7..d0de33892c4 100644 --- a/qa/qa/page/README.md +++ b/qa/qa/page/README.md @@ -70,15 +70,15 @@ module Page module Main class Login < Page::Base view 'app/views/devise/passwords/edit.html.haml' do - element :password_field, 'password_field :password' - element :password_confirmation, 'password_field :password_confirmation' - element :change_password_button, 'submit "Change your password"' + element :password_field + element :password_confirmation + element :change_password_button end view 'app/views/devise/sessions/_new_base.html.haml' do - element :login_field, 'text_field :login' - element :password_field, 'password_field :password' - element :sign_in_button, 'submit "Sign in"' + element :login_field + element :password_field + element :sign_in_button end # ... @@ -86,20 +86,32 @@ module Page end ``` -It is possible to use `element` DSL method without value, with a String value -or with a Regexp. +The `view` DSL method declares the filename of the view where an +`element` is implemented. + +The `element` DSL method in turn declares an element for which a corresponding +`qa-element-name-dasherized` CSS class need to be added to the view file. + +You can also define a value (String or Regexp) to match to the actual view +code but **this is deprecated** in favor of the above method for two reasons: + +- Consistency: there is only one way to define an element +- Separation of concerns: QA uses dedicated CSS classes instead of reusing code + or classes used by other components (e.g. `js-*` classes etc.) ```ruby view 'app/views/my/view.html.haml' do + # Implicitly require `.qa-logout-button` CSS class to be present in the view + element :logout_button + + ## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop. # Require `f.submit "Sign in"` to be present in `my/view.html.haml - element :my_button, 'f.submit "Sign in"' + element :my_button, 'f.submit "Sign in"' # rubocop:disable QA/ElementWithPattern + ## This is deprecated and forbidden by the `QA/ElementWithPattern` RuboCop cop. # Match every line in `my/view.html.haml` against # `/link_to .* "My Profile"/` regexp. - element :profile_link, /link_to .* "My Profile"/ - - # Implicitly require `.qa-logout-button` CSS class to be present in the view - element :logout_button + element :profile_link, /link_to .* "My Profile"/ # rubocop:disable QA/ElementWithPattern end ``` @@ -119,4 +131,4 @@ If you need more information, ask for help on `#quality` channel on Slack (internal, GitLab Team only). If you are not a Team Member, and you still need help to contribute, please -open an issue in GitLab QA issue tracker. +open an issue in GitLab CE issue tracker with the `~QA` label. diff --git a/qa/qa/page/admin/menu.rb b/qa/qa/page/admin/menu.rb new file mode 100644 index 00000000000..e8c7d274966 --- /dev/null +++ b/qa/qa/page/admin/menu.rb @@ -0,0 +1,47 @@ +# frozen_string_literal: true + +module QA + module Page + module Admin + class Menu < Page::Base + view 'app/views/layouts/nav/sidebar/_admin.html.haml' do + element :admin_sidebar + element :admin_sidebar_submenu + element :admin_settings_item + element :admin_settings_repository_item + end + + def go_to_repository_settings + hover_settings do + within_submenu do + click_element :admin_settings_repository_item + end + end + end + + private + + def hover_settings + within_sidebar do + scroll_to_element(:admin_settings_item) + find_element(:admin_settings_item).hover + + yield + end + end + + def within_sidebar + within_element(:admin_sidebar) do + yield + end + end + + def within_submenu + within_element(:admin_sidebar_submenu) do + yield + end + end + end + end + end +end diff --git a/qa/qa/page/admin/settings/component/repository_storage.rb b/qa/qa/page/admin/settings/component/repository_storage.rb new file mode 100644 index 00000000000..2ed0cd73aa3 --- /dev/null +++ b/qa/qa/page/admin/settings/component/repository_storage.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Page + module Admin + module Settings + module Component + class RepositoryStorage < Page::Base + view 'app/views/admin/application_settings/_repository_storage.html.haml' do + element :hashed_storage_checkbox + element :save_changes_button + end + + def enable_hashed_storage + check_element :hashed_storage_checkbox + end + + def save_settings + click_element :save_changes_button + end + end + end + end + end + end +end diff --git a/qa/qa/page/admin/settings/main.rb b/qa/qa/page/admin/settings/main.rb deleted file mode 100644 index 73034ffe0d8..00000000000 --- a/qa/qa/page/admin/settings/main.rb +++ /dev/null @@ -1,21 +0,0 @@ -module QA - module Page - module Admin - module Settings - class Main < Page::Base - include QA::Page::Settings::Common - - view 'app/views/admin/application_settings/show.html.haml' do - element :terms_settings - end - - def expand_repository_storage(&block) - expand_section(:terms_settings) do - RepositoryStorage.perform(&block) - end - end - end - end - end - end -end diff --git a/qa/qa/page/admin/settings/repository.rb b/qa/qa/page/admin/settings/repository.rb new file mode 100644 index 00000000000..b7f1deb21bd --- /dev/null +++ b/qa/qa/page/admin/settings/repository.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Admin + module Settings + class Repository < Page::Base + include QA::Page::Settings::Common + + view 'app/views/admin/application_settings/repository.html.haml' do + element :repository_storage_settings + end + + def expand_repository_storage(&block) + expand_section(:repository_storage_settings) do + Component::RepositoryStorage.perform(&block) + end + end + end + end + end + end +end diff --git a/qa/qa/page/admin/settings/repository_storage.rb b/qa/qa/page/admin/settings/repository_storage.rb deleted file mode 100644 index 68dd23a41e1..00000000000 --- a/qa/qa/page/admin/settings/repository_storage.rb +++ /dev/null @@ -1,23 +0,0 @@ -module QA - module Page - module Admin - module Settings - class RepositoryStorage < Page::Base - view 'app/views/admin/application_settings/_repository_storage.html.haml' do - element :submit, "submit 'Save changes'" - element :hashed_storage, - 'Use hashed storage paths for newly created and renamed projects' - end - - def enable_hashed_storage - check 'Use hashed storage paths for newly created and renamed projects' - end - - def save_settings - click_button 'Save changes' - end - end - end - end - end -end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 30e35bf7abb..91e229c4c8c 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -1,12 +1,17 @@ +# frozen_string_literal: true + require 'capybara/dsl' module QA module Page class Base + prepend Support::Page::Logging if Runtime::Env.debug? include Capybara::DSL include Scenario::Actable extend SingleForwardable + ElementNotFound = Class.new(RuntimeError) + def_delegators :evaluator, :view, :views def refresh @@ -28,6 +33,21 @@ module QA false end + def with_retry(max_attempts: 3, reload: false) + attempts = 0 + + while attempts < max_attempts + result = yield + return result if result + + refresh if reload + + attempts += 1 + end + + false + end + def scroll_to(selector, text: nil) page.execute_script <<~JS var elements = Array.from(document.querySelectorAll('#{selector}')); @@ -68,6 +88,10 @@ module QA all(element_selector_css(name)) end + def check_element(name) + find_element(name).set(true) + end + def click_element(name) find_element(name).click end @@ -76,12 +100,20 @@ module QA find_element(name).set(content) end + def has_element?(name) + has_css?(element_selector_css(name)) + end + def within_element(name) page.within(element_selector_css(name)) do yield end end + def scroll_to_element(name, *args) + scroll_to(element_selector_css(name), *args) + end + def element_selector_css(name) Page::Element.new(name).selector_css end diff --git a/qa/qa/page/component/clone_panel.rb b/qa/qa/page/component/clone_panel.rb index 8e8ff4e3bb0..94e761b0e0c 100644 --- a/qa/qa/page/component/clone_panel.rb +++ b/qa/qa/page/component/clone_panel.rb @@ -7,8 +7,8 @@ module QA def self.included(base) base.view 'app/views/shared/_clone_panel.html.haml' do element :clone_dropdown - element :clone_options_dropdown, '.clone-options-dropdown' - element :project_repository_location, 'text_field_tag :project_clone' + element :clone_options_dropdown, '.clone-options-dropdown' # rubocop:disable QA/ElementWithPattern + element :project_repository_location, 'text_field_tag :project_clone' # rubocop:disable QA/ElementWithPattern end end diff --git a/qa/qa/page/component/dropdown_filter.rb b/qa/qa/page/component/dropdown_filter.rb new file mode 100644 index 00000000000..e896c382779 --- /dev/null +++ b/qa/qa/page/component/dropdown_filter.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module DropdownFilter + def filter_and_select(item) + wait(reload: false) do + page.has_css?('.dropdown-input-field') + end + + find('.dropdown-input-field').set(item) + click_link item + end + end + end + end +end diff --git a/qa/qa/page/component/groups_filter.rb b/qa/qa/page/component/groups_filter.rb index e647d368f0f..cc50bb439b4 100644 --- a/qa/qa/page/component/groups_filter.rb +++ b/qa/qa/page/component/groups_filter.rb @@ -6,12 +6,7 @@ module QA module GroupsFilter def self.included(base) base.view 'app/views/shared/groups/_search_form.html.haml' do - element :groups_filter, 'search_field_tag :filter' - element :groups_filter_placeholder, 'Search by name' - end - - base.view 'app/views/shared/groups/_empty_state.html.haml' do - element :groups_empty_state + element :groups_filter end base.view 'app/assets/javascripts/groups/components/groups.vue' do @@ -21,13 +16,22 @@ module QA private - def filter_by_name(name) + def has_filtered_group?(name) + # Filter and submit to reload the page and only retrieve the filtered results + find_element(:groups_filter).set(name).send_keys(:return) + + # Since we submitted after filtering, the presence of + # groups_list_tree_container means we have the complete filtered list + # of groups wait(reload: false) do - page.has_css?(element_selector_css(:groups_empty_state)) || - page.has_css?(element_selector_css(:groups_list_tree_container)) + page.has_css?(element_selector_css(:groups_list_tree_container)) end - fill_in 'Search by name', with: name + # If there are no groups we'll know immediately because we filtered the list + return false if page.has_text?('No groups or projects matched your search', wait: 0) + + # The name will be present as filter input so we check for a link, not text + page.has_link?(name, wait: 0) end end end diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb new file mode 100644 index 00000000000..cfd8ac1e7c8 --- /dev/null +++ b/qa/qa/page/component/issuable/common.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module Issuable + module Common + def self.included(base) + base.view 'app/assets/javascripts/issue_show/components/title.vue' do + element :edit_button + end + + base.view 'app/assets/javascripts/issue_show/components/fields/title.vue' do + element :title_input + end + + base.view 'app/assets/javascripts/issue_show/components/fields/description.vue' do + element :description_textarea + end + + base.view 'app/assets/javascripts/issue_show/components/edit_actions.vue' do + element :save_button + element :delete_button + end + + base.view 'app/assets/javascripts/issue_show/components/edit_actions.vue' do + element :save_button + element :delete_button + end + end + end + end + end + end +end diff --git a/qa/qa/page/component/select2.rb b/qa/qa/page/component/select2.rb index 30829eb0221..6d07d5a10e6 100644 --- a/qa/qa/page/component/select2.rb +++ b/qa/qa/page/component/select2.rb @@ -3,7 +3,12 @@ module QA module Component module Select2 def select_item(item_text) - find('ul.select2-result-sub > li', text: item_text).click + find('.select2-result-label', text: item_text).click + end + + def search_and_select(item_text) + find('.select2-input').set(item_text) + select_item(item_text) end end end diff --git a/qa/qa/page/component/users_select.rb b/qa/qa/page/component/users_select.rb new file mode 100644 index 00000000000..f88d6450a33 --- /dev/null +++ b/qa/qa/page/component/users_select.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module UsersSelect + def select_user(element, username) + find("#{element_selector_css(element)} input").set(username) + find('.ajax-users-dropdown .user-username', text: "@#{username}").click + end + end + end + end +end diff --git a/qa/qa/page/dashboard/groups.rb b/qa/qa/page/dashboard/groups.rb index 70c5f996ff8..7a07515de62 100644 --- a/qa/qa/page/dashboard/groups.rb +++ b/qa/qa/page/dashboard/groups.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module QA module Page module Dashboard @@ -5,18 +7,16 @@ module QA include Page::Component::GroupsFilter view 'app/views/shared/groups/_search_form.html.haml' do - element :groups_filter, 'search_field_tag :filter' - element :groups_filter_placeholder, 'Search by name' + element :groups_filter, 'search_field_tag :filter' # rubocop:disable QA/ElementWithPattern + element :groups_filter_placeholder, 'Search by name' # rubocop:disable QA/ElementWithPattern end view 'app/views/dashboard/_groups_head.html.haml' do - element :new_group_button, 'link_to _("New group")' + element :new_group_button, 'link_to _("New group")' # rubocop:disable QA/ElementWithPattern end def has_group?(name) - filter_by_name(name) - - page.has_link?(name) + has_filtered_group?(name) end def go_to_group(name) diff --git a/qa/qa/page/dashboard/projects.rb b/qa/qa/page/dashboard/projects.rb index 5b2827c089c..0f434577b3b 100644 --- a/qa/qa/page/dashboard/projects.rb +++ b/qa/qa/page/dashboard/projects.rb @@ -5,7 +5,7 @@ module QA view 'app/views/dashboard/projects/index.html.haml' view 'app/views/shared/projects/_search_form.html.haml' do - element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/ + element :form_filter_by_name, /form_tag.+id: 'project-filter-form'/ # rubocop:disable QA/ElementWithPattern end def go_to_project(name) diff --git a/qa/qa/page/file/form.rb b/qa/qa/page/file/form.rb index f6e502f500b..a1534231691 100644 --- a/qa/qa/page/file/form.rb +++ b/qa/qa/page/file/form.rb @@ -3,14 +3,23 @@ module QA module File class Form < Page::Base include Shared::CommitMessage + include Page::Component::DropdownFilter view 'app/views/projects/blob/_editor.html.haml' do - element :file_name, "text_field_tag 'file_name'" - element :editor, '#editor' + element :file_name, "text_field_tag 'file_name'" # rubocop:disable QA/ElementWithPattern + element :editor, '#editor' # rubocop:disable QA/ElementWithPattern end view 'app/views/projects/_commit_button.html.haml' do - element :commit_changes, "button_tag 'Commit changes'" + element :commit_changes, "button_tag 'Commit changes'" # rubocop:disable QA/ElementWithPattern + end + + view 'app/views/projects/blob/_template_selectors.html.haml' do + element :template_type_dropdown + element :gitignore_dropdown + element :gitlab_ci_yml_dropdown + element :dockerfile_dropdown + element :license_dropdown end def add_name(name) @@ -29,6 +38,25 @@ module QA click_on 'Commit changes' end + def select_template(template_type, template) + click_element :template_type_dropdown + click_link template_type + + case template_type + when '.gitignore' + click_element :gitignore_dropdown + when '.gitlab-ci.yml' + click_element :gitlab_ci_yml_dropdown + when 'Dockerfile' + click_element :dockerfile_dropdown + when 'LICENSE' + click_element :license_dropdown + else + raise %Q(Unsupported template_type "#{template_type}". Please confirm that it is a valid option.) + end + filter_and_select template + end + private def text_area diff --git a/qa/qa/page/file/shared/commit_message.rb b/qa/qa/page/file/shared/commit_message.rb index 5af1a55e2ef..aa1bb081cd3 100644 --- a/qa/qa/page/file/shared/commit_message.rb +++ b/qa/qa/page/file/shared/commit_message.rb @@ -5,7 +5,7 @@ module QA module CommitMessage def self.included(base) base.view 'app/views/shared/_commit_message_container.html.haml' do - element :commit_message, "text_area_tag 'commit_message'" + element :commit_message, "text_area_tag 'commit_message'" # rubocop:disable QA/ElementWithPattern end end diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb index 99f5924b67f..abd8ebb089f 100644 --- a/qa/qa/page/file/show.rb +++ b/qa/qa/page/file/show.rb @@ -5,12 +5,12 @@ module QA include Shared::CommitMessage view 'app/helpers/blob_helper.rb' do - element :edit_button, "_('Edit')" - element :delete_button, /label:\s+"Delete"/ + element :edit_button, "_('Edit')" # rubocop:disable QA/ElementWithPattern + element :delete_button, /label:\s+"Delete"/ # rubocop:disable QA/ElementWithPattern end view 'app/views/projects/blob/_remove.html.haml' do - element :delete_file_button, "button_tag 'Delete file'" + element :delete_file_button, "button_tag 'Delete file'" # rubocop:disable QA/ElementWithPattern end def click_edit diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index 48b71a7c883..39584daf334 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -3,14 +3,14 @@ module QA module Group class New < Page::Base view 'app/views/shared/_group_form.html.haml' do - element :group_path_field, 'text_field :path' - element :group_name_field, 'text_field :name' - element :group_description_field, 'text_area :description' + element :group_path_field, 'text_field :path' # rubocop:disable QA/ElementWithPattern + element :group_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern + element :group_description_field, 'text_area :description' # rubocop:disable QA/ElementWithPattern end view 'app/views/groups/new.html.haml' do - element :create_group_button, "submit 'Create group'" - element :visibility_radios, 'visibility_level:' + element :create_group_button, "submit 'Create group'" # rubocop:disable QA/ElementWithPattern + element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern end def set_path(path) diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb index 6747f7f10b6..0f0ab81a4ef 100644 --- a/qa/qa/page/group/show.rb +++ b/qa/qa/page/group/show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module QA module Page module Group @@ -5,18 +7,15 @@ module QA include Page::Component::GroupsFilter view 'app/views/groups/show.html.haml' do - element :new_project_or_subgroup_dropdown, '.new-project-subgroup' - element :new_project_or_subgroup_dropdown_toggle, '.dropdown-toggle' - element :new_project_option, /%li.*data:.*value: "new-project"/ - element :new_project_button, /%input.*data:.*action: "new-project"/ - element :new_subgroup_option, /%li.*data:.*value: "new-subgroup"/ - - # data-value and data-action get modified by JS for subgroup - element :new_subgroup_button, /%input.*\.js-new-group-child/ + element :new_project_or_subgroup_dropdown + element :new_project_or_subgroup_dropdown_toggle + element :new_project_option + element :new_subgroup_option + element :new_in_group_button end view 'app/assets/javascripts/groups/constants.js' do - element :no_result_text, 'No groups or projects matched your search' + element :no_result_text, 'No groups or projects matched your search' # rubocop:disable QA/ElementWithPattern end def go_to_subgroup(name) @@ -24,43 +23,37 @@ module QA end def has_new_project_or_subgroup_dropdown? - page.has_css?(element_selector_css(:new_project_or_subgroup_dropdown)) + has_element?(:new_project_or_subgroup_dropdown) end def has_subgroup?(name) - filter_by_name(name) - - page.has_text?(/#{name}|No groups or projects matched your search/, wait: 60) - - page.has_text?(name, wait: 0) + has_filtered_group?(name) end def go_to_new_subgroup - click_new('subgroup') + select_kind :new_subgroup_option - find("input[data-action='new-subgroup']").click + click_element :new_in_group_button end def go_to_new_project - click_new('project') + select_kind :new_project_option - find("input[data-action='new-project']").click + click_element :new_in_group_button end private - def click_new(kind) - within '.new-project-subgroup' do - css = "li[data-value='new-#{kind}']" - + def select_kind(kind) + within_element(:new_project_or_subgroup_dropdown) do # May need to click again because it is possible to click the button quicker than the JS is bound wait(reload: false) do - find('.dropdown-toggle').click + click_element :new_project_or_subgroup_dropdown_toggle - page.has_css?(css) + has_element?(kind) end - find(css).click + click_element kind end end end diff --git a/qa/qa/page/issuable/sidebar.rb b/qa/qa/page/issuable/sidebar.rb index f207264e24f..d3751b712c9 100644 --- a/qa/qa/page/issuable/sidebar.rb +++ b/qa/qa/page/issuable/sidebar.rb @@ -3,8 +3,8 @@ module QA module Issuable class Sidebar < Page::Base view 'app/views/shared/issuable/_sidebar.html.haml' do - element :labels_block, ".issuable-show-labels" - element :milestones_block, '.block.milestone' + element :labels_block, ".issuable-show-labels" # rubocop:disable QA/ElementWithPattern + element :milestones_block, '.block.milestone' # rubocop:disable QA/ElementWithPattern end def has_label?(label) diff --git a/qa/qa/page/label/index.rb b/qa/qa/page/label/index.rb new file mode 100644 index 00000000000..323acd57743 --- /dev/null +++ b/qa/qa/page/label/index.rb @@ -0,0 +1,15 @@ +module QA + module Page + module Label + class Index < Page::Base + view 'app/views/projects/labels/index.html.haml' do + element :label_create_new + end + + def go_to_new_label + click_element :label_create_new + end + end + end + end +end diff --git a/qa/qa/page/label/new.rb b/qa/qa/page/label/new.rb new file mode 100644 index 00000000000..b5422dc9400 --- /dev/null +++ b/qa/qa/page/label/new.rb @@ -0,0 +1,30 @@ +module QA + module Page + module Label + class New < Page::Base + view 'app/views/shared/labels/_form.html.haml' do + element :label_title + element :label_description + element :label_color + element :label_create_button + end + + def create_label + click_element :label_create_button + end + + def fill_title(title) + fill_element :label_title, title + end + + def fill_description(description) + fill_element :label_description, description + end + + def fill_color(color) + fill_element :label_color, color + end + end + end + end +end diff --git a/qa/qa/page/layout/banner.rb b/qa/qa/page/layout/banner.rb index e7654bdafc9..2223f2adec8 100644 --- a/qa/qa/page/layout/banner.rb +++ b/qa/qa/page/layout/banner.rb @@ -3,7 +3,7 @@ module QA module Layout class Banner < Page::Base view 'app/views/layouts/header/_read_only_banner.html.haml' do - element :flash_notice, ".flash-notice" + element :flash_notice, ".flash-notice" # rubocop:disable QA/ElementWithPattern end def has_notice?(message) diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index e9e49964e63..97ffe0e5716 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -23,6 +23,7 @@ module QA view 'app/views/devise/shared/_tabs_ldap.html.haml' do element :ldap_tab element :standard_tab + element :register_tab end view 'app/views/devise/shared/_tabs_normal.html.haml' do @@ -30,19 +31,23 @@ module QA element :register_tab end + view 'app/views/devise/shared/_omniauth_box.html.haml' do + element :saml_login_button + end + def initialize # The login page is usually the entry point for all the scenarios so # we need to wait for the instance to start. That said, in some cases # we are already logged-in so we check both cases here. wait(max: 500) do - page.has_css?('.login-page') || - Page::Menu::Main.act { has_personal_area?(wait: 0) } + has_css?('.login-page') || + Page::Main::Menu.act { has_personal_area?(wait: 0) } end end def sign_in_using_credentials(user = nil) # Don't try to log-in if we're already logged-in - return if Page::Menu::Main.act { has_personal_area?(wait: 0) } + return if Page::Main::Menu.act { has_personal_area?(wait: 0) } using_wait_time 0 do set_initial_password_if_present @@ -56,11 +61,11 @@ module QA end end - Page::Menu::Main.act { has_personal_area? } + Page::Main::Menu.act { has_personal_area? } 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 @@ -71,19 +76,35 @@ module QA sign_in_using_gitlab_credentials(admin) end - Page::Menu::Main.act { has_personal_area? } + Page::Main::Menu.act { has_personal_area? } end def self.path '/users/sign_in' end + def has_sign_in_tab? + has_element?(:sign_in_tab) + end + + def has_ldap_tab? + has_element?(:ldap_tab) + end + + def has_standard_tab? + has_element?(:standard_tab) + end + def sign_in_tab? - page.has_button?('Sign in') + has_css?(".active", text: 'Sign in') end def ldap_tab? - page.has_link?('LDAP') + has_css?(".active", text: 'LDAP') + end + + def standard_tab? + has_css?(".active", text: 'Standard') end def switch_to_sign_in_tab @@ -91,6 +112,7 @@ module QA end def switch_to_register_tab + set_initial_password_if_present click_element :register_tab end @@ -112,9 +134,14 @@ module QA click_element :sign_in_button end + def sign_in_with_saml + set_initial_password_if_present + click_element :saml_login_button + end + def sign_in_using_gitlab_credentials(user) - switch_to_sign_in_tab unless sign_in_tab? - switch_to_standard_tab if ldap_tab? + switch_to_sign_in_tab if has_sign_in_tab? + switch_to_standard_tab if has_standard_tab? fill_element :login_field, user.username fill_element :password_field, user.password @@ -122,7 +149,7 @@ module QA end def set_initial_password_if_present - return unless page.has_content?('Change your password') + return unless has_content?('Change your password') fill_element :password_field, Runtime::User.password fill_element :password_confirmation, Runtime::User.password diff --git a/qa/qa/page/menu/main.rb b/qa/qa/page/main/menu.rb index 36e7285f7b7..cc2724618e9 100644 --- a/qa/qa/page/menu/main.rb +++ b/qa/qa/page/main/menu.rb @@ -1,16 +1,18 @@ +# frozen_string_literal: true + module QA module Page - module Menu - class Main < Page::Base + module Main + class Menu < Page::Base view 'app/views/layouts/header/_current_user_dropdown.html.haml' do - element :user_sign_out_link, 'link_to _("Sign out")' - element :settings_link, 'link_to s_("CurrentUser|Settings")' + element :user_sign_out_link, 'link_to _("Sign out")' # rubocop:disable QA/ElementWithPattern + element :settings_link, 'link_to s_("CurrentUser|Settings")' # rubocop:disable QA/ElementWithPattern end view 'app/views/layouts/header/_default.html.haml' do element :navbar element :user_avatar - element :user_menu, '.dropdown-menu' + element :user_menu, '.dropdown-menu' # rubocop:disable QA/ElementWithPattern end view 'app/views/layouts/nav/_dashboard.html.haml' do @@ -61,8 +63,15 @@ module QA end def has_personal_area?(wait: Capybara.default_max_wait_time) - # No need to wait, either we're logged-in, or not. - using_wait_time(wait) { page.has_selector?('.qa-user-avatar') } + using_wait_time(wait) do + page.has_selector?(element_selector_css(:user_avatar)) + end + end + + def has_admin_area_link?(wait: Capybara.default_max_wait_time) + using_wait_time(wait) do + page.has_selector?(element_selector_css(:admin_area_link)) + end end private diff --git a/qa/qa/page/main/oauth.rb b/qa/qa/page/main/oauth.rb index 618f114e058..bc44d274314 100644 --- a/qa/qa/page/main/oauth.rb +++ b/qa/qa/page/main/oauth.rb @@ -3,7 +3,7 @@ module QA module Main class OAuth < Page::Base view 'app/views/doorkeeper/authorizations/new.html.haml' do - element :authorization_button, 'submit_tag _("Authorize")' + element :authorization_button, 'submit_tag _("Authorize")' # rubocop:disable QA/ElementWithPattern end def needs_authorization? diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb index 33ab56236f4..9ca498012eb 100644 --- a/qa/qa/page/main/sign_up.rb +++ b/qa/qa/page/main/sign_up.rb @@ -1,25 +1,35 @@ +# frozen_string_literal: true + module QA module Page module Main class SignUp < Page::Base view 'app/views/devise/shared/_signup_box.html.haml' do - element :name, 'text_field :name' - element :username, 'text_field :username' - element :email_field, 'email_field :email' - element :email_confirmation, 'email_field :email_confirmation' - element :password, 'password_field :password' - element :register_button, 'submit "Register"' + element :new_user_name + element :new_user_username + element :new_user_email + element :new_user_email_confirmation + element :new_user_password + element :new_user_register_button + element :new_user_accept_terms end def sign_up!(user) - fill_in :new_user_name, with: user.name - fill_in :new_user_username, with: user.username - fill_in :new_user_email, with: user.email - fill_in :new_user_email_confirmation, with: user.email - fill_in :new_user_password, with: user.password - click_button 'Register' + fill_element :new_user_name, user.name + fill_element :new_user_username, user.username + fill_element :new_user_email, user.email + fill_element :new_user_email_confirmation, user.email + fill_element :new_user_password, user.password + + check_element :new_user_accept_terms if has_element?(:new_user_accept_terms) + + signed_in = with_retry do + click_element :new_user_register_button + + Page::Main::Menu.act { has_personal_area? } + end - Page::Menu::Main.act { has_personal_area? } + raise "Failed to register and sign in" unless signed_in end end end diff --git a/qa/qa/page/menu/admin.rb b/qa/qa/page/menu/admin.rb deleted file mode 100644 index 573b98f7386..00000000000 --- a/qa/qa/page/menu/admin.rb +++ /dev/null @@ -1,15 +0,0 @@ -module QA - module Page - module Menu - class Admin < Page::Base - view 'app/views/layouts/nav/sidebar/_admin.html.haml' do - element :settings, "_('Settings')" - end - - def go_to_settings - click_link 'Settings' - end - end - end - end -end diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb index 83cc4bbbace..1f8f1fbca8e 100644 --- a/qa/qa/page/merge_request/new.rb +++ b/qa/qa/page/merge_request/new.rb @@ -22,6 +22,10 @@ module QA element :issuable_dropdown_menu_milestone end + view 'app/views/shared/issuable/_label_dropdown.html.haml' do + element :issuable_label + end + def create_merge_request click_element :issuable_create_button end @@ -40,6 +44,12 @@ module QA click_on milestone.title end end + + def select_label(label) + click_element :issuable_label + + click_link label.title + end end end end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index befb7c1809a..2fd30e15ffb 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -1,28 +1,64 @@ +# frozen_string_literal: true + module QA module Page module MergeRequest class Show < Page::Base view 'app/assets/javascripts/vue_merge_request_widget/components/states/ready_to_merge.vue' do element :merge_button - element :fast_forward_message, 'Fast-forward merge without a merge commit' + element :fast_forward_message, 'Fast-forward merge without a merge commit' # rubocop:disable QA/ElementWithPattern element :merge_moment_dropdown element :merge_when_pipeline_succeeds_option element :merge_immediately_option end view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do - element :merged_status, 'The changes were merged into' + element :merged_status, 'The changes were merged into' # rubocop:disable QA/ElementWithPattern end view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do element :mr_rebase_button - element :no_fast_forward_message, 'Fast-forward merge is not possible' + element :no_fast_forward_message, 'Fast-forward merge is not possible' # rubocop:disable QA/ElementWithPattern end - view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_squash_before_merge.vue' do + view 'app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue' do element :squash_checkbox end + view 'app/views/projects/merge_requests/show.html.haml' do + element :notes_tab + element :diffs_tab + end + + view 'app/assets/javascripts/diffs/components/diff_line_gutter_content.vue' do + element :diff_comment + end + + view 'app/assets/javascripts/notes/components/comment_form.vue' do + element :note_dropdown + element :discussion_option + end + + view 'app/assets/javascripts/notes/components/note_form.vue' do + element :reply_input + end + + view 'app/assets/javascripts/notes/components/noteable_discussion.vue' do + element :discussion_reply + end + + view 'app/assets/javascripts/diffs/components/inline_diff_table_row.vue' do + element :new_diff_line + end + + view 'app/views/shared/issuable/_sidebar.html.haml' do + element :labels_block + end + + view 'app/views/projects/merge_requests/_mr_title.html.haml' do + element :edit_button + end + def fast_forward_possible? !has_text?('Fast-forward merge is not possible') end @@ -64,6 +100,13 @@ module QA end end + def has_label?(label) + page.within(element_selector_css(:labels_block)) do + element = find('span', text: label) + !element.nil? + end + end + def merge! # The merge button is disabled on load wait do @@ -95,6 +138,39 @@ module QA click_element :squash_checkbox end + + def go_to_discussions_tab + click_element :notes_tab + end + + def go_to_diffs_tab + click_element :diffs_tab + end + + def add_comment_to_diff(text) + wait(time: 5) do + page.has_text?("No newline at end of file") + end + all_elements(:new_diff_line).first.hover + click_element :diff_comment + fill_element :reply_input, text + end + + def start_discussion(text) + fill_element :comment_input, text + click_element :note_dropdown + click_element :discussion_option + click_element :comment_button + end + + def reply_to_discussion(reply_text) + all_elements(:discussion_reply).last.click + fill_element :reply_input, reply_text + end + + def edit! + click_element :edit_button + end end end end diff --git a/qa/qa/page/menu/profile.rb b/qa/qa/page/profile/menu.rb index 7e24fa85c33..2d503499e13 100644 --- a/qa/qa/page/menu/profile.rb +++ b/qa/qa/page/profile/menu.rb @@ -1,12 +1,14 @@ +# frozen_string_literal: true + module QA module Page - module Menu - class Profile < Page::Base + module Profile + class Menu < Page::Base view 'app/views/layouts/nav/sidebar/_profile.html.haml' do - element :access_token_link, 'link_to profile_personal_access_tokens_path' - element :access_token_title, 'Access Tokens' - element :top_level_items, '.sidebar-top-level-items' - element :ssh_keys, 'SSH Keys' + element :access_token_link, 'link_to profile_personal_access_tokens_path' # rubocop:disable QA/ElementWithPattern + element :access_token_title, 'Access Tokens' # rubocop:disable QA/ElementWithPattern + element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern + element :ssh_keys, 'SSH Keys' # rubocop:disable QA/ElementWithPattern end def click_access_tokens diff --git a/qa/qa/page/profile/personal_access_tokens.rb b/qa/qa/page/profile/personal_access_tokens.rb index f5ae47dadd0..9191dbe9cf3 100644 --- a/qa/qa/page/profile/personal_access_tokens.rb +++ b/qa/qa/page/profile/personal_access_tokens.rb @@ -3,13 +3,13 @@ module QA module Profile class PersonalAccessTokens < Page::Base view 'app/views/shared/_personal_access_tokens_form.html.haml' do - element :personal_access_token_name_field, 'text_field :name' - element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable Lint/InterpolationCheck - element :scopes_api_radios, "label :scopes" + element :personal_access_token_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern + element :create_token_button, 'submit "Create #{type} token"' # rubocop:disable QA/ElementWithPattern, Lint/InterpolationCheck + element :scopes_api_radios, "label :scopes" # rubocop:disable QA/ElementWithPattern end - view 'app/views/profiles/personal_access_tokens/index.html.haml' do - element :create_token_field, "text_field_tag 'created-personal-access-token'" + view 'app/views/shared/_personal_access_tokens_created_container.html.haml' do + element :create_token_field, "text_field_tag 'created-personal-access-token'" # rubocop:disable QA/ElementWithPattern end def fill_token_name(name) diff --git a/qa/qa/page/project/activity.rb b/qa/qa/page/project/activity.rb index 0196922c889..56fbaa90790 100644 --- a/qa/qa/page/project/activity.rb +++ b/qa/qa/page/project/activity.rb @@ -3,7 +3,7 @@ module QA module Project class Activity < Page::Base view 'app/views/shared/_event_filter.html.haml' do - element :push_events, "event_filter_link EventFilter.push, _('Push events')" + element :push_events, "event_filter_link EventFilter::PUSH, _('Push events')" # rubocop:disable QA/ElementWithPattern end def go_to_push_events diff --git a/qa/qa/page/project/fork/new.rb b/qa/qa/page/project/fork/new.rb index ed92df956bf..140c004b458 100644 --- a/qa/qa/page/project/fork/new.rb +++ b/qa/qa/page/project/fork/new.rb @@ -4,7 +4,7 @@ module QA module Fork class New < Page::Base view 'app/views/projects/forks/_fork_button.html.haml' do - element :namespace, 'link_to project_forks_path' + element :namespace, 'link_to project_forks_path' # rubocop:disable QA/ElementWithPattern end def choose_namespace(namespace = Runtime::Namespace.path) diff --git a/qa/qa/page/project/import/github.rb b/qa/qa/page/project/import/github.rb index 1a410a0f8a5..a3cde73d3f2 100644 --- a/qa/qa/page/project/import/github.rb +++ b/qa/qa/page/project/import/github.rb @@ -6,16 +6,16 @@ module QA include Page::Component::Select2 view 'app/views/import/github/new.html.haml' do - element :personal_access_token_field, 'text_field_tag :personal_access_token' - element :list_repos_button, "submit_tag _('List your GitHub repositories')" + element :personal_access_token_field, 'text_field_tag :personal_access_token' # rubocop:disable QA/ElementWithPattern + element :list_repos_button, "submit_tag _('List your GitHub repositories')" # rubocop:disable QA/ElementWithPattern end view 'app/views/import/_githubish_status.html.haml' do - element :project_import_row, 'data: { qa: { repo_path: repo.full_name } }' + element :project_import_row, 'data: { qa: { repo_path: repo.full_name } }' # rubocop:disable QA/ElementWithPattern element :project_namespace_select - element :project_namespace_field, 'select_tag :namespace_id' - element :project_path_field, 'text_field_tag :path, sanitize_project_name(repo.name)' - element :import_button, "_('Import')" + element :project_namespace_field, 'select_tag :namespace_id' # rubocop:disable QA/ElementWithPattern + element :project_path_field, 'text_field_tag :path, sanitize_project_name(repo.name)' # rubocop:disable QA/ElementWithPattern + element :import_button, "_('Import')" # rubocop:disable QA/ElementWithPattern end def add_personal_access_token(personal_access_token) diff --git a/qa/qa/page/project/issue/index.rb b/qa/qa/page/project/issue/index.rb index b5903f536a4..1035bf74a43 100644 --- a/qa/qa/page/project/issue/index.rb +++ b/qa/qa/page/project/issue/index.rb @@ -4,7 +4,7 @@ module QA module Issue class Index < Page::Base view 'app/views/projects/issues/_issue.html.haml' do - element :issue_link, 'link_to issue.title' + element :issue_link, 'link_to issue.title' # rubocop:disable QA/ElementWithPattern end def go_to_issue(title) diff --git a/qa/qa/page/project/issue/new.rb b/qa/qa/page/project/issue/new.rb index 7fc581da1ed..03b605ab24b 100644 --- a/qa/qa/page/project/issue/new.rb +++ b/qa/qa/page/project/issue/new.rb @@ -4,15 +4,15 @@ module QA module Issue class New < Page::Base view 'app/views/shared/issuable/_form.html.haml' do - element :submit_issue_button, 'form.submit "Submit' + element :submit_issue_button, 'form.submit "Submit' # rubocop:disable QA/ElementWithPattern end view 'app/views/shared/issuable/form/_title.html.haml' do - element :issue_title_textbox, 'form.text_field :title' + element :issue_title_textbox, 'form.text_field :title' # rubocop:disable QA/ElementWithPattern end view 'app/views/shared/form_elements/_description.html.haml' do - element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description" + element :issue_description_textarea, "render 'projects/zen', f: form, attr: :description" # rubocop:disable QA/ElementWithPattern end def add_title(title) diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index 587a02163b9..23def93c7dd 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -5,35 +5,49 @@ module QA module Project module Issue class Show < Page::Base - view 'app/views/projects/issues/show.html.haml' do - element :issue_details, '.issue-details' - element :title, '.title' - end + include Page::Component::Issuable::Common view 'app/views/shared/notes/_form.html.haml' do - element :new_note_form, 'new-note' - element :new_note_form, 'attr: :note' + element :new_note_form, 'new-note' # rubocop:disable QA/ElementWithPattern + element :new_note_form, 'attr: :note' # rubocop:disable QA/ElementWithPattern end - view 'app/views/shared/notes/_comment_button.html.haml' do - element :comment_button, '%strong Comment' + view 'app/assets/javascripts/notes/components/comment_form.vue' do + element :comment_button + element :comment_input end - def issue_title - find('.issue-details .title').text + view 'app/assets/javascripts/notes/components/discussion_filter.vue' do + element :discussion_filter + element :filter_options end # Adds a comment to an issue # attachment option should be an absolute path def comment(text, attachment: nil) - fill_in(with: text, name: 'note[note]') + fill_element :comment_input, text unless attachment.nil? QA::Page::Component::Dropzone.new(self, '.new-note') .attach_file(attachment) end - click_on 'Comment' + click_element :comment_button + end + + def select_comments_only_filter + click_element :discussion_filter + all_elements(:filter_options)[1].click + end + + def select_history_only_filter + click_element :discussion_filter + all_elements(:filter_options).last.click + end + + def select_all_activities_filter + click_element :discussion_filter + all_elements(:filter_options).first.click end end end diff --git a/qa/qa/page/project/job/show.rb b/qa/qa/page/project/job/show.rb index 228ffd9d381..d688f15914c 100644 --- a/qa/qa/page/project/job/show.rb +++ b/qa/qa/page/project/job/show.rb @@ -4,30 +4,39 @@ module QA::Page COMPLETED_STATUSES = %w[passed failed canceled blocked skipped manual].freeze # excludes created, pending, running PASSED_STATUS = 'passed'.freeze - view 'app/views/shared/builds/_build_output.html.haml' do - element :build_output, '.js-build-output' - element :loading_animation, '.js-build-refresh' + view 'app/assets/javascripts/jobs/components/job_app.vue' do + element :loading_animation + end + + view 'app/assets/javascripts/jobs/components/job_log.vue' do + element :build_trace end view 'app/assets/javascripts/vue_shared/components/ci_badge_link.vue' do - element :status_badge, 'ci-status' + element :status_badge end def completed? - COMPLETED_STATUSES.include? find('.ci-status').text + COMPLETED_STATUSES.include?(status_badge) end def passed? - find('.ci-status').text == PASSED_STATUS + status_badge == PASSED_STATUS end def trace_loading? - has_css?('.js-build-refresh') + has_element?(:loading_animation) end # Reminder: You may wish to wait for a particular job status before checking output def output - find('.js-build-output').text + find_element(:build_trace).text + end + + private + + def status_badge + find_element(:status_badge).text end end end diff --git a/qa/qa/page/menu/side.rb b/qa/qa/page/project/menu.rb index a1eedfea42e..cb4a10e1b6a 100644 --- a/qa/qa/page/menu/side.rb +++ b/qa/qa/page/project/menu.rb @@ -1,27 +1,32 @@ +# frozen_string_literal: true + module QA module Page - module Menu - class Side < Page::Base + module Project + class Menu < Page::Base view 'app/views/layouts/nav/sidebar/_project.html.haml' do element :settings_item - element :settings_link, 'link_to edit_project_path' - element :repository_link, "title: _('Repository')" + element :settings_link, 'link_to edit_project_path' # rubocop:disable QA/ElementWithPattern + element :repository_link, "title: _('Repository')" # rubocop:disable QA/ElementWithPattern element :link_pipelines - element :pipelines_settings_link, "title: _('CI / CD')" - element :operations_kubernetes_link, "title: _('Kubernetes')" - element :issues_link, /link_to.*shortcuts-issues/ - element :issues_link_text, "Issues" - element :merge_requests_link, /link_to.*shortcuts-merge_requests/ - element :merge_requests_link_text, "Merge Requests" - element :top_level_items, '.sidebar-top-level-items' - element :operations_section, "class: 'shortcuts-operations'" - element :activity_link, "title: _('Activity')" - element :wiki_link_text, "Wiki" + element :link_members_settings + element :pipelines_settings_link, "title: _('CI / CD')" # rubocop:disable QA/ElementWithPattern + element :operations_kubernetes_link, "title: _('Kubernetes')" # rubocop:disable QA/ElementWithPattern + element :operations_environments_link + element :issues_link, /link_to.*shortcuts-issues/ # rubocop:disable QA/ElementWithPattern + element :issues_link_text, "Issues" # rubocop:disable QA/ElementWithPattern + element :merge_requests_link, /link_to.*shortcuts-merge_requests/ # rubocop:disable QA/ElementWithPattern + element :merge_requests_link_text, "Merge Requests" # rubocop:disable QA/ElementWithPattern + element :top_level_items, '.sidebar-top-level-items' # rubocop:disable QA/ElementWithPattern + element :operations_section, "class: 'shortcuts-operations'" # rubocop:disable QA/ElementWithPattern + element :activity_link, "title: _('Activity')" # rubocop:disable QA/ElementWithPattern + element :wiki_link_text, "Wiki" # rubocop:disable QA/ElementWithPattern element :milestones_link + element :labels_link end view 'app/assets/javascripts/fly_out_nav.js' do - element :fly_out, "classList.add('fly-out-list')" + element :fly_out, "classList.add('fly-out-list')" # rubocop:disable QA/ElementWithPattern end def click_repository_settings @@ -40,6 +45,22 @@ module QA end end + def click_operations_environments + hover_operations do + within_submenu do + click_element(:operations_environments_link) + end + end + end + + def click_members_settings + hover_settings do + within_submenu do + click_element :link_members_settings + end + end + end + def click_operations_kubernetes hover_operations do within_submenu do @@ -66,6 +87,14 @@ module QA end end + def go_to_labels + hover_issues do + within_submenu do + click_element(:labels_link) + end + end + end + def click_merge_requests within_sidebar do click_link('Merge Requests') @@ -84,8 +113,22 @@ module QA end end + def click_repository + within_sidebar do + click_link('Repository') + end + end + private + def hover_issues + within_sidebar do + find_element(:issues_item).hover + + yield + end + end + def hover_settings within_sidebar do find('.qa-settings-item').hover diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 0766c98da6f..6acc413b586 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -5,21 +5,21 @@ module QA include Page::Component::Select2 view 'app/views/projects/new.html.haml' do - element :import_project_tab, "Import project" + element :import_project_tab, "Import project" # rubocop:disable QA/ElementWithPattern end view 'app/views/projects/_new_project_fields.html.haml' do element :project_namespace_select - element :project_namespace_field, 'namespaces_options' - element :project_name, 'text_field :name' - element :project_path, 'text_field :path' - element :project_description, 'text_area :description' - element :project_create_button, "submit 'Create project'" - element :visibility_radios, 'visibility_level:' + element :project_namespace_field, 'namespaces_options' # rubocop:disable QA/ElementWithPattern + element :project_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern + element :project_path, 'text_field :path' # rubocop:disable QA/ElementWithPattern + element :project_description, 'text_area :description' # rubocop:disable QA/ElementWithPattern + element :project_create_button, "submit 'Create project'" # rubocop:disable QA/ElementWithPattern + element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern end view 'app/views/projects/_import_project_pane.html.haml' do - element :import_github, "icon('github', text: 'GitHub')" + element :import_github, "icon('github', text: 'GitHub')" # rubocop:disable QA/ElementWithPattern end def choose_test_namespace diff --git a/qa/qa/page/project/operations/environments/index.rb b/qa/qa/page/project/operations/environments/index.rb new file mode 100644 index 00000000000..63965a57edd --- /dev/null +++ b/qa/qa/page/project/operations/environments/index.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Operations + module Environments + class Index < Page::Base + view 'app/assets/javascripts/environments/components/environment_item.vue' do + element :environment_link + end + + def go_to_environment(environment_name) + wait(reload: false) do + find(element_selector_css(:environment_link), text: environment_name).click + end + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/operations/environments/show.rb b/qa/qa/page/project/operations/environments/show.rb new file mode 100644 index 00000000000..aa88c218c89 --- /dev/null +++ b/qa/qa/page/project/operations/environments/show.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Operations + module Environments + class Show < Page::Base + view 'app/views/projects/environments/_external_url.html.haml' do + element :view_deployment + end + + def view_deployment(&block) + new_window = window_opened_by { click_element(:view_deployment) } + + within_window(new_window, &block) if block + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/operations/kubernetes/add.rb b/qa/qa/page/project/operations/kubernetes/add.rb index 11ebe10fb18..939f912ea85 100644 --- a/qa/qa/page/project/operations/kubernetes/add.rb +++ b/qa/qa/page/project/operations/kubernetes/add.rb @@ -4,8 +4,8 @@ module QA module Operations module Kubernetes class Add < Page::Base - view 'app/views/projects/clusters/new.html.haml' do - element :add_existing_cluster_button, "Add existing cluster" + view 'app/views/clusters/clusters/new.html.haml' do + element :add_existing_cluster_button, "Add existing cluster" # rubocop:disable QA/ElementWithPattern end def add_existing_cluster diff --git a/qa/qa/page/project/operations/kubernetes/add_existing.rb b/qa/qa/page/project/operations/kubernetes/add_existing.rb index eef82b5f329..f3ab636ecc1 100644 --- a/qa/qa/page/project/operations/kubernetes/add_existing.rb +++ b/qa/qa/page/project/operations/kubernetes/add_existing.rb @@ -4,12 +4,13 @@ module QA module Operations module Kubernetes class AddExisting < Page::Base - view 'app/views/projects/clusters/user/_form.html.haml' do - element :cluster_name, 'text_field :name' - element :api_url, 'text_field :api_url' - element :ca_certificate, 'text_area :ca_cert' - element :token, 'text_field :token' - element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" + view 'app/views/clusters/clusters/user/_form.html.haml' do + element :cluster_name, 'text_field :name' # rubocop:disable QA/ElementWithPattern + element :api_url, 'text_field :api_url' # rubocop:disable QA/ElementWithPattern + element :ca_certificate, 'text_area :ca_cert' # rubocop:disable QA/ElementWithPattern + element :token, 'text_field :token' # rubocop:disable QA/ElementWithPattern + element :add_cluster_button, "submit s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern + element :rbac_checkbox end def set_cluster_name(name) @@ -31,6 +32,10 @@ module QA def add_cluster! click_on 'Add Kubernetes cluster' end + + def check_rbac! + check_element :rbac_checkbox + end end end end diff --git a/qa/qa/page/project/operations/kubernetes/index.rb b/qa/qa/page/project/operations/kubernetes/index.rb index 7261b5645da..67a74af1cd2 100644 --- a/qa/qa/page/project/operations/kubernetes/index.rb +++ b/qa/qa/page/project/operations/kubernetes/index.rb @@ -4,8 +4,8 @@ module QA module Operations module Kubernetes class Index < Page::Base - view 'app/views/projects/clusters/_empty_state.html.haml' do - element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" + view 'app/views/clusters/clusters/_empty_state.html.haml' do + element :add_kubernetes_cluster_button, "link_to s_('ClusterIntegration|Add Kubernetes cluster')" # rubocop:disable QA/ElementWithPattern end def add_kubernetes_cluster diff --git a/qa/qa/page/project/operations/kubernetes/show.rb b/qa/qa/page/project/operations/kubernetes/show.rb index e831edeb89e..9e8f9ba79d7 100644 --- a/qa/qa/page/project/operations/kubernetes/show.rb +++ b/qa/qa/page/project/operations/kubernetes/show.rb @@ -5,13 +5,13 @@ module QA module Kubernetes class Show < Page::Base view 'app/assets/javascripts/clusters/components/application_row.vue' do - element :application_row, 'js-cluster-application-row-${this.id}' - element :install_button, "s__('ClusterIntegration|Install')" - element :installed_button, "s__('ClusterIntegration|Installed')" + element :application_row, 'js-cluster-application-row-${this.id}' # rubocop:disable QA/ElementWithPattern + element :install_button, "s__('ClusterIntegration|Install')" # rubocop:disable QA/ElementWithPattern + element :installed_button, "s__('ClusterIntegration|Installed')" # rubocop:disable QA/ElementWithPattern end view 'app/assets/javascripts/clusters/components/applications.vue' do - element :ingress_ip_address, 'id="ingress-ip-address"' + element :ingress_ip_address, 'id="ingress-ip-address"' # rubocop:disable QA/ElementWithPattern end def install!(application_name) diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb index ce430a2a6ee..19d83ecc4f4 100644 --- a/qa/qa/page/project/pipeline/index.rb +++ b/qa/qa/page/project/pipeline/index.rb @@ -2,7 +2,7 @@ module QA::Page module Project::Pipeline class Index < QA::Page::Base view 'app/assets/javascripts/pipelines/components/pipeline_url.vue' do - element :pipeline_link, 'class="js-pipeline-url-link"' + element :pipeline_link, 'class="js-pipeline-url-link"' # rubocop:disable QA/ElementWithPattern end def go_to_latest_pipeline diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index babc0079f3f..b22396fd67a 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -2,20 +2,20 @@ module QA::Page module Project::Pipeline class Show < QA::Page::Base view 'app/assets/javascripts/vue_shared/components/header_ci_component.vue' do - element :pipeline_header, /header class.*ci-header-container.*/ + element :pipeline_header, /header class.*ci-header-container.*/ # rubocop:disable QA/ElementWithPattern end view 'app/assets/javascripts/pipelines/components/graph/graph_component.vue' do - element :pipeline_graph, /class.*pipeline-graph.*/ + element :pipeline_graph, /class.*pipeline-graph.*/ # rubocop:disable QA/ElementWithPattern end - view 'app/assets/javascripts/pipelines/components/graph/job_component.vue' do - element :job_component, /class.*ci-job-component.*/ - element :job_link, /class.*js-pipeline-graph-job-link.*/ + view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do + element :job_component, /class.*ci-job-component.*/ # rubocop:disable QA/ElementWithPattern + element :job_link, /class.*js-pipeline-graph-job-link.*/ # rubocop:disable QA/ElementWithPattern end view 'app/assets/javascripts/vue_shared/components/ci_icon.vue' do - element :status_icon, 'ci-status-icon-${status}' + element :status_icon, 'ci-status-icon-${status}' # rubocop:disable QA/ElementWithPattern end def running? diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb index d7b2b66b587..578f097e2dc 100644 --- a/qa/qa/page/project/settings/advanced.rb +++ b/qa/qa/page/project/settings/advanced.rb @@ -4,9 +4,9 @@ module QA module Settings class Advanced < Page::Base view 'app/views/projects/edit.html.haml' do - element :project_path_field, 'text_field :path' - element :project_name_field, 'text_field :name' - element :rename_project_button, "submit 'Rename project'" + element :project_path_field, 'text_field :path' # rubocop:disable QA/ElementWithPattern + element :project_name_field, 'text_field :name' # rubocop:disable QA/ElementWithPattern + element :rename_project_button, "submit 'Rename project'" # rubocop:disable QA/ElementWithPattern end def rename_to(path) diff --git a/qa/qa/page/project/settings/ci_cd.rb b/qa/qa/page/project/settings/ci_cd.rb index 752d3d93407..12c2409a5a7 100644 --- a/qa/qa/page/project/settings/ci_cd.rb +++ b/qa/qa/page/project/settings/ci_cd.rb @@ -12,11 +12,11 @@ module QA # rubocop:disable Naming/FileName end view 'app/views/projects/settings/ci_cd/_autodevops_form.html.haml' do - element :enable_auto_devops_field, 'check_box :enabled' - element :domain_field, 'text_field :domain' - element :enable_auto_devops_button, "%strong= s_('CICD|Default to Auto DevOps pipeline')" - element :domain_input, "%strong= _('Domain')" - element :save_changes_button, "submit _('Save changes')" + element :enable_auto_devops_field, 'check_box :enabled' # rubocop:disable QA/ElementWithPattern + element :domain_field, 'text_field :domain' # rubocop:disable QA/ElementWithPattern + element :enable_auto_devops_button, "%strong= s_('CICD|Default to Auto DevOps pipeline')" # rubocop:disable QA/ElementWithPattern + element :domain_input, "%strong= _('Domain')" # rubocop:disable QA/ElementWithPattern + element :save_changes_button, "submit _('Save changes')" # rubocop:disable QA/ElementWithPattern end def expand_runners_settings(&block) @@ -25,9 +25,9 @@ module QA # rubocop:disable Naming/FileName end end - def expand_secret_variables(&block) + def expand_ci_variables(&block) expand_section(:variables_settings) do - Settings::SecretVariables.perform(&block) + Settings::CiVariables.perform(&block) end end diff --git a/qa/qa/page/project/settings/secret_variables.rb b/qa/qa/page/project/settings/ci_variables.rb index 937ae6797c8..e7a6e4bf628 100644 --- a/qa/qa/page/project/settings/secret_variables.rb +++ b/qa/qa/page/project/settings/ci_variables.rb @@ -2,18 +2,18 @@ module QA module Page module Project module Settings - class SecretVariables < Page::Base + class CiVariables < Page::Base include Common view 'app/views/ci/variables/_variable_row.html.haml' do - element :variable_row, '.ci-variable-row-body' - element :variable_key, '.qa-ci-variable-input-key' - element :variable_value, '.qa-ci-variable-input-value' + element :variable_row, '.ci-variable-row-body' # rubocop:disable QA/ElementWithPattern + element :variable_key, '.qa-ci-variable-input-key' # rubocop:disable QA/ElementWithPattern + element :variable_value, '.qa-ci-variable-input-value' # rubocop:disable QA/ElementWithPattern end view 'app/views/ci/variables/_index.html.haml' do - element :save_variables, '.js-secret-variables-save-button' - element :reveal_values, '.js-secret-value-reveal-button' + element :save_variables, '.js-ci-variables-save-button' # rubocop:disable QA/ElementWithPattern + element :reveal_values, '.js-secret-value-reveal-button' # rubocop:disable QA/ElementWithPattern end def fill_variable(key, value) @@ -33,7 +33,7 @@ module QA end def save_variables - find('.js-secret-variables-save-button').click + find('.js-ci-variables-save-button').click end def reveal_variables diff --git a/qa/qa/page/project/settings/common.rb b/qa/qa/page/project/settings/common.rb index 874fb381554..f3b217677f2 100644 --- a/qa/qa/page/project/settings/common.rb +++ b/qa/qa/page/project/settings/common.rb @@ -8,7 +8,7 @@ module QA def self.included(base) base.class_eval do view 'app/views/projects/edit.html.haml' do - element :advanced_settings_expand, "= expanded ? 'Collapse' : 'Expand'" + element :advanced_settings_expand, "= expanded ? 'Collapse' : 'Expand'" # rubocop:disable QA/ElementWithPattern end end end diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index 90a0e7092bd..3c8c0cbdf7c 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -4,18 +4,18 @@ module QA module Settings class DeployKeys < Page::Base view 'app/views/projects/deploy_keys/_form.html.haml' do - element :deploy_key_title, 'text_field :title' - element :deploy_key_key, 'text_area :key' + element :deploy_key_title, 'text_field :title' # rubocop:disable QA/ElementWithPattern + element :deploy_key_key, 'text_area :key' # rubocop:disable QA/ElementWithPattern end view 'app/assets/javascripts/deploy_keys/components/app.vue' do - element :deploy_keys_section, /class=".*deploy\-keys.*"/ - element :project_deploy_keys, 'class="qa-project-deploy-keys"' + element :deploy_keys_section, /class=".*deploy\-keys.*"/ # rubocop:disable QA/ElementWithPattern + element :project_deploy_keys, 'class="qa-project-deploy-keys"' # rubocop:disable QA/ElementWithPattern end view 'app/assets/javascripts/deploy_keys/components/key.vue' do - element :key_title, /class=".*qa-key-title.*"/ - element :key_fingerprint, /class=".*qa-key-fingerprint.*"/ + element :key_title, /class=".*qa-key-title.*"/ # rubocop:disable QA/ElementWithPattern + element :key_fingerprint, /class=".*qa-key-fingerprint.*"/ # rubocop:disable QA/ElementWithPattern end def fill_key_title(title) diff --git a/qa/qa/page/project/settings/deploy_tokens.rb b/qa/qa/page/project/settings/deploy_tokens.rb new file mode 100644 index 00000000000..2d42372cbc5 --- /dev/null +++ b/qa/qa/page/project/settings/deploy_tokens.rb @@ -0,0 +1,64 @@ +module QA + module Page + module Project + module Settings + class DeployTokens < Page::Base + view 'app/views/projects/deploy_tokens/_form.html.haml' do + element :deploy_token_name + element :deploy_token_expires_at + element :deploy_token_read_repository + element :deploy_token_read_registry + element :create_deploy_token + end + + view 'app/views/projects/deploy_tokens/_new_deploy_token.html.haml' do + element :created_deploy_token_section + element :deploy_token_user + element :deploy_token + end + + def fill_token_name(name) + fill_element :deploy_token_name, name + end + + def fill_token_expires_at(expires_at) + fill_element :deploy_token_expires_at, expires_at.to_s + "\n" + end + + def fill_scopes(read_repository:, read_registry:) + check_element :deploy_token_read_repository if read_repository + check_element :deploy_token_read_registry if read_registry + end + + def add_token + click_element :create_deploy_token + end + + def token_username + within_new_project_deploy_token do + find_element(:deploy_token_user).value + end + end + + def token_password + within_new_project_deploy_token do + find_element(:deploy_token).value + end + end + + private + + def within_new_project_deploy_token + wait(reload: false) do + has_css?(element_selector_css(:created_deploy_token_section)) + end + + within_element(:created_deploy_token_section) do + yield + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/members.rb b/qa/qa/page/project/settings/members.rb new file mode 100644 index 00000000000..7fed93ca83f --- /dev/null +++ b/qa/qa/page/project/settings/members.rb @@ -0,0 +1,27 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + class Members < Page::Base + include Page::Component::UsersSelect + + view 'app/views/projects/project_members/_new_project_member.html.haml' do + element :member_select_input + element :add_member_button + end + + view 'app/views/projects/project_members/_team.html.haml' do + element :members_list + end + + def add_member(username) + select_user :member_select_input, username + click_element :add_member_button + end + end + end + end + end +end diff --git a/qa/qa/page/project/settings/repository.rb b/qa/qa/page/project/settings/repository.rb index 1ed5f455a85..53ebe28970b 100644 --- a/qa/qa/page/project/settings/repository.rb +++ b/qa/qa/page/project/settings/repository.rb @@ -24,6 +24,12 @@ module QA ProtectedBranches.perform(&block) end end + + def expand_deploy_tokens(&block) + expand_section(:deploy_tokens_settings) do + DeployTokens.perform(&block) + end + end end end end diff --git a/qa/qa/page/project/settings/runners.rb b/qa/qa/page/project/settings/runners.rb index b41668c94cd..ac930f5385a 100644 --- a/qa/qa/page/project/settings/runners.rb +++ b/qa/qa/page/project/settings/runners.rb @@ -4,8 +4,8 @@ module QA module Settings class Runners < Page::Base view 'app/views/ci/runner/_how_to_setup_runner.html.haml' do - element :registration_token, '%code#registration_token' - element :coordinator_address, '%code#coordinator_address' + element :registration_token, '%code#registration_token' # rubocop:disable QA/ElementWithPattern + element :coordinator_address, '%code#coordinator_address' # rubocop:disable QA/ElementWithPattern end ## @@ -13,7 +13,7 @@ module QA # view 'app/helpers/runners_helper.rb' do # rubocop:disable Lint/InterpolationCheck - element :runner_status, 'runner-status-#{status}' + element :runner_status, 'runner-status-#{status}' # rubocop:disable QA/ElementWithPattern # rubocop:enable Lint/InterpolationCheck end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 267e7bbc249..d6dddf03ffb 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -1,3 +1,5 @@ +# frozen_string_literal: true + module QA module Page module Project @@ -14,7 +16,7 @@ module QA view 'app/views/layouts/header/_new_dropdown.haml' do element :new_menu_toggle - element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" + element :new_issue_link, "link_to _('New issue'), new_project_issue_path(@project)" # rubocop:disable QA/ElementWithPattern end view 'app/views/shared/_ref_switcher.html.haml' do @@ -23,24 +25,40 @@ module QA end view 'app/views/projects/buttons/_fork.html.haml' do - element :fork_label, "%span= s_('ProjectOverview|Fork')" - element :fork_link, "link_to new_project_fork_path(@project)" + element :fork_label, "%span= s_('ProjectOverview|Fork')" # rubocop:disable QA/ElementWithPattern + element :fork_link, "link_to new_project_fork_path(@project)" # rubocop:disable QA/ElementWithPattern end view 'app/views/projects/_files.html.haml' do - element :tree_holder, '.tree-holder' + element :tree_holder, '.tree-holder' # rubocop:disable QA/ElementWithPattern + end + + view 'app/views/projects/buttons/_dropdown.html.haml' do + element :create_new_dropdown + element :new_file_option + end + + view 'app/views/projects/tree/_tree_header.html.haml' do + element :web_ide_button end - view 'app/presenters/project_presenter.rb' do - element :new_file_button, "_('New file')," + view 'app/views/projects/tree/_tree_content.html.haml' do + element :file_tree end def project_name find('.qa-project-name').text end - def go_to_new_file! - click_on 'New file' + def create_new_file! + click_element :create_new_dropdown + click_element :new_file_option + end + + def go_to_file(filename) + within_element(:file_tree) do + click_on filename + end end def switch_to_branch(branch_name) @@ -78,6 +96,10 @@ module QA def fork_project click_on 'Fork' end + + def open_web_ide! + click_element :web_ide_button + end end end end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb new file mode 100644 index 00000000000..23e580b81b6 --- /dev/null +++ b/qa/qa/page/project/web_ide/edit.rb @@ -0,0 +1,79 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module WebIDE + class Edit < Page::Base + include Page::Component::DropdownFilter + + view 'app/assets/javascripts/ide/components/ide_tree.vue' do + element :new_file + end + + view 'app/assets/javascripts/ide/components/ide_tree_list.vue' do + element :file_list + end + + view 'app/assets/javascripts/ide/components/new_dropdown/modal.vue' do + element :full_file_path + element :template_list + end + + view 'app/assets/javascripts/ide/components/file_templates/bar.vue' do + element :file_templates_bar + element :file_template_dropdown + end + + view 'app/assets/javascripts/ide/components/file_templates/dropdown.vue' do + element :dropdown_filter_input + end + + view 'app/assets/javascripts/ide/components/commit_sidebar/form.vue' do + element :begin_commit_button + element :commit_button + end + + def has_file?(file_name) + within_element(:file_list) do + page.has_content? file_name + end + end + + def create_new_file_from_template(file_name, template) + click_element :new_file + within_element(:template_list) do + begin + click_on file_name + rescue Capybara::ElementNotFound + raise ElementNotFound, %Q(Couldn't find file template named "#{file_name}". Please confirm that it is a valid option.) + end + end + + wait(reload: false) do + within_element(:file_templates_bar) do + click_element :file_template_dropdown + fill_element :dropdown_filter_input, template + + begin + click_on template + rescue Capybara::ElementNotFound + raise ElementNotFound, %Q(Couldn't find template "#{template}" for #{file_name}. Please confirm that it exists in the list of templates.) + end + end + end + end + + def commit_changes + click_element :begin_commit_button + click_element :commit_button + + wait(reload: false) do + page.has_content?('Your changes have been committed') + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/wiki/edit.rb b/qa/qa/page/project/wiki/edit.rb index 6fa45569cc0..8d0eafa1818 100644 --- a/qa/qa/page/project/wiki/edit.rb +++ b/qa/qa/page/project/wiki/edit.rb @@ -4,9 +4,9 @@ module QA module Wiki class Edit < Page::Base view 'app/views/projects/wikis/_main_links.html.haml' do - element :new_page_link, 'New page' - element :page_history_link, 'Page history' - element :edit_page_link, 'Edit' + element :new_page_link, 'New page' # rubocop:disable QA/ElementWithPattern + element :page_history_link, 'Page history' # rubocop:disable QA/ElementWithPattern + element :edit_page_link, 'Edit' # rubocop:disable QA/ElementWithPattern end def go_to_new_page diff --git a/qa/qa/page/project/wiki/new.rb b/qa/qa/page/project/wiki/new.rb index 415b3835538..2498af8600c 100644 --- a/qa/qa/page/project/wiki/new.rb +++ b/qa/qa/page/project/wiki/new.rb @@ -4,15 +4,15 @@ module QA module Wiki class New < Page::Base view 'app/views/projects/wikis/_form.html.haml' do - element :wiki_title_textbox, 'text_field :title' - element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content" - element :wiki_message_textbox, 'text_field :message' - element :save_changes_button, 'submit _("Save changes")' - element :create_page_button, 'submit s_("Wiki|Create page")' + element :wiki_title_textbox, 'text_field :title' # rubocop:disable QA/ElementWithPattern + element :wiki_content_textarea, "render 'projects/zen', f: f, attr: :content" # rubocop:disable QA/ElementWithPattern + element :wiki_message_textbox, 'text_field :message' # rubocop:disable QA/ElementWithPattern + element :save_changes_button, 'submit _("Save changes")' # rubocop:disable QA/ElementWithPattern + element :create_page_button, 'submit s_("Wiki|Create page")' # rubocop:disable QA/ElementWithPattern end view 'app/views/shared/empty_states/_wikis.html.haml' do - element :create_link, 'Create your first page' + element :create_link, 'Create your first page' # rubocop:disable QA/ElementWithPattern end def go_to_create_first_page diff --git a/qa/qa/page/project/wiki/show.rb b/qa/qa/page/project/wiki/show.rb index c47a715687f..a7c4455d080 100644 --- a/qa/qa/page/project/wiki/show.rb +++ b/qa/qa/page/project/wiki/show.rb @@ -8,7 +8,7 @@ module QA include Page::Component::ClonePanel view 'app/views/projects/wikis/pages.html.haml' do - element :clone_repository_link, 'Clone repository' + element :clone_repository_link, 'Clone repository' # rubocop:disable QA/ElementWithPattern end def go_to_clone_repository 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..98eebac0880 --- /dev/null +++ b/qa/qa/resource/api_fabricator.rb @@ -0,0 +1,96 @@ +# frozen_string_literal: true + +require 'active_support/core_ext/object/deep_dup' +require 'capybara/dsl' + +module QA + module Resource + module ApiFabricator + 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 + + include Support::Api + 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 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..dcea144ab74 --- /dev/null +++ b/qa/qa/resource/base.rb @@ -0,0 +1,155 @@ +# frozen_string_literal: true + +require 'forwardable' +require 'capybara/dsl' +require 'active_support/core_ext/array/extract_options' + +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..a7a6f931e28 --- /dev/null +++ b/qa/qa/resource/group.rb @@ -0,0 +1,72 @@ +# 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_members_path + "#{api_get_path}/members" + 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..77afb3cfcba --- /dev/null +++ b/qa/qa/resource/merge_request.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class MergeRequest < Base + attr_accessor :title, + :description, + :source_branch, + :target_branch, + :assignee, + :milestone, + :labels, + :file_name, + :file_content + + 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 = file_name + resource.file_content = file_content + 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 = [] + @file_name = "added_file.txt" + @file_content = "File Added" + 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/factory/repository/project_push.rb b/qa/qa/resource/repository/project_push.rb index 167f47c9141..c9fafe3419f 100644 --- a/qa/qa/factory/repository/project_push.rb +++ b/qa/qa/resource/repository/project_push.rb @@ -1,18 +1,14 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class ProjectPush < Factory::Repository::Push - dependency Factory::Resource::Project, as: :project do |project| - project.name = 'project-with-code' - project.description = 'Project with repository' - end - - product :output do |factory| - factory.output - end - - product :project do |factory| - factory.project + 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 diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/resource/repository/push.rb index 6c5088f1da5..c14d97ff7fb 100644 --- a/qa/qa/factory/repository/push.rb +++ b/qa/qa/resource/repository/push.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + require 'pathname' module QA - module Factory + module Resource module Repository - class Push < Factory::Base + 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 @@ -30,6 +32,14 @@ module QA @directory = dir end + def files=(files) + if !files.is_a?(Array) || files.empty? + raise ArgumentError, "Please provide an array of hashes e.g.: [{name: 'file1', content: 'foo'}]" + end + + @files = files + end + def fabricate! Git::Repository.perform do |repository| if ssh_key @@ -37,7 +47,7 @@ module QA repository.use_ssh_key(ssh_key) else repository.uri = repository_http_uri - repository.use_default_credentials + repository.use_default_credentials unless user end username = 'GitLab QA' @@ -63,6 +73,10 @@ module QA @directory.each_child do |f| repository.add_file(f.basename, f.read) if f.file? end + elsif @files + @files.each do |f| + repository.add_file(f[:name], f[:content]) + end else repository.add_file(file_name, file_content) end diff --git a/qa/qa/factory/repository/wiki_push.rb b/qa/qa/resource/repository/wiki_push.rb index ecc6cc18c88..f1c39d507fe 100644 --- a/qa/qa/factory/repository/wiki_push.rb +++ b/qa/qa/resource/repository/wiki_push.rb @@ -1,11 +1,15 @@ +# frozen_string_literal: true + module QA - module Factory + module Resource module Repository - class WikiPush < Factory::Repository::Push - dependency Factory::Resource::Wiki, as: :wiki do |wiki| - wiki.title = 'Home' - wiki.content = '# My First Wiki Content' - wiki.message = 'Update home' + 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 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..c26f0c84a1f --- /dev/null +++ b/qa/qa/resource/user.rb @@ -0,0 +1,113 @@ +# frozen_string_literal: true + +require 'securerandom' + +module QA + module Resource + class User < Base + attr_reader :unique_id + attr_writer :username, :password, :name, :email + attr_accessor :provider, :extern_uid + + 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 + }.merge(ldap_post_body) + end + + def self.fabricate_or_use(username, password) + if Runtime::Env.signup_disabled? + self.new.tap do |user| + user.username = username + user.password = password + end + else + self.fabricate! + end + end + + private + + def ldap_post_body + return {} unless extern_uid && provider + + { + extern_uid: extern_uid, + provider: provider + } + end + + 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 02015e23ad8..aff84c89f0e 100644 --- a/qa/qa/runtime/api/client.rb +++ b/qa/qa/runtime/api/client.rb @@ -6,33 +6,34 @@ module QA class Client attr_reader :address - def initialize(address = :gitlab, personal_access_token: nil) + def initialize(address = :gitlab, personal_access_token: nil, is_new_session: true) @address = address @personal_access_token = personal_access_token + @is_new_session = is_new_session end def personal_access_token - @personal_access_token ||= get_personal_access_token - end - - def get_personal_access_token - # you can set the environment variable PERSONAL_ACCESS_TOKEN - # to use a specific access token rather than create one from the UI - if Runtime::Env.personal_access_token - Runtime::Env.personal_access_token - else - create_personal_access_token + @personal_access_token ||= begin + # you can set the environment variable PERSONAL_ACCESS_TOKEN + # to use a specific access token rather than create one from the UI + Runtime::Env.personal_access_token ||= create_personal_access_token end end private def create_personal_access_token - Runtime::Browser.visit(@address, Page::Main::Login) do - Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::PersonalAccessToken.fabricate!.access_token + if @is_new_session + Runtime::Browser.visit(@address, Page::Main::Login) { do_create_personal_access_token } + else + do_create_personal_access_token end end + + def do_create_personal_access_token + Page::Main::Login.act { sign_in_using_credentials } + Resource::PersonalAccessToken.fabricate!.access_token + end end end end diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 4c64270ce92..7fd2ba25527 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -51,6 +51,10 @@ module QA } ) + if QA::Runtime::Env.accept_insecure_certs? + capabilities['acceptInsecureCerts'] = true + end + options = Selenium::WebDriver::Chrome::Options.new options.add_argument("window-size=1240,1680") @@ -113,6 +117,15 @@ module QA def perform(&block) visit(url) + if QA::Runtime::Env.qa_cookies + browser = Capybara.current_session.driver.browser + QA::Runtime::Env.qa_cookies.each do |cookie| + name, value = cookie.split("=") + value ||= "" + browser.manage.add_cookie name: name, value: value + end + end + yield.tap { clear! } if block_given? end diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 27ba915961d..3bc2b44ccd8 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -1,20 +1,54 @@ +# frozen_string_literal: true + module QA module Runtime module Env extend self + attr_writer :personal_access_token, :ldap_username, :ldap_password + + # The environment variables used to indicate if the environment under test + # supports the given feature + SUPPORTED_FEATURES = { + git_protocol_v2: 'QA_CAN_TEST_GIT_PROTOCOL_V2' + }.freeze + + def supported_features + SUPPORTED_FEATURES + end + + def debug? + enabled?(ENV['QA_DEBUG'], default: false) + end + + def log_destination + ENV['QA_LOG_PATH'] || $stdout + end + # set to 'false' to have Chrome run visibly instead of headless def chrome_headless? - (ENV['CHROME_HEADLESS'] =~ /^(false|no|0)$/i) != 0 + enabled?(ENV['CHROME_HEADLESS']) + end + + def accept_insecure_certs? + enabled?(ENV['ACCEPT_INSECURE_CERTS']) end def running_in_ci? ENV['CI'] || ENV['CI_SERVER'] end + def qa_cookies + ENV['QA_COOKIES'] && ENV['QA_COOKIES'].split(';') + end + + def signup_disabled? + enabled?(ENV['SIGNUP_DISABLED'], default: false) + end + # specifies token that can be used for the api def personal_access_token - ENV['PERSONAL_ACCESS_TOKEN'] + @personal_access_token ||= ENV['PERSONAL_ACCESS_TOKEN'] end def user_username @@ -34,7 +68,7 @@ module QA end def forker? - forker_username && forker_password + !!(forker_username && forker_password) end def forker_username @@ -45,18 +79,42 @@ module QA ENV['GITLAB_FORKER_PASSWORD'] end + def gitlab_qa_username_1 + ENV['GITLAB_QA_USERNAME_1'] || 'gitlab-qa-user1' + end + + def gitlab_qa_password_1 + ENV['GITLAB_QA_PASSWORD_1'] + end + + def gitlab_qa_username_2 + ENV['GITLAB_QA_USERNAME_2'] || 'gitlab-qa-user2' + end + + def gitlab_qa_password_2 + ENV['GITLAB_QA_PASSWORD_2'] + end + def ldap_username - ENV['GITLAB_LDAP_USERNAME'] + @ldap_username ||= ENV['GITLAB_LDAP_USERNAME'] end def ldap_password - ENV['GITLAB_LDAP_PASSWORD'] + @ldap_password ||= ENV['GITLAB_LDAP_PASSWORD'] end def sandbox_name ENV['GITLAB_SANDBOX_NAME'] end + def namespace_name + ENV['GITLAB_NAMESPACE_NAME'] + end + + def auto_devops_project_name + ENV['GITLAB_AUTO_DEVOPS_PROJECT_NAME'] + end + def gcloud_account_key ENV.fetch("GCLOUD_ACCOUNT_KEY") end @@ -83,6 +141,23 @@ module QA raise ArgumentError, "Please provide GITHUB_ACCESS_TOKEN" end + + # Returns true if there is an environment variable that indicates that + # the feature is supported in the environment under test. + # All features are supported by default. + def can_test?(feature) + raise ArgumentError, %Q(Unknown feature "#{feature}") unless SUPPORTED_FEATURES.include? feature + + enabled?(ENV[SUPPORTED_FEATURES[feature]], default: true) + end + + private + + def enabled?(value, default: true) + return default if value.nil? + + (value =~ /^(false|no|0)$/i) != 0 + end end end end diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb new file mode 100644 index 00000000000..72004d5b00a --- /dev/null +++ b/qa/qa/runtime/fixtures.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module QA + module Runtime + module Fixtures + def fetch_template_from_api(api_path, key) + request = Runtime::API::Request.new(api_client, "/templates/#{api_path}/#{key}") + get request.url + json_body[:content] + end + + private + + def api_client + @api_client ||= Runtime::API::Client.new(:gitlab) + end + end + end +end diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb new file mode 100644 index 00000000000..bd5c4fe5bf5 --- /dev/null +++ b/qa/qa/runtime/logger.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +require 'logger' + +module QA + module Runtime + module Logger + extend SingleForwardable + + def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown + + singleton_class.module_eval do + attr_writer :logger + + def logger + return @logger if @logger + + @logger = ::Logger.new Runtime::Env.log_destination + @logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR + @logger + end + end + end + end +end diff --git a/qa/qa/runtime/namespace.rb b/qa/qa/runtime/namespace.rb index f1c8ef11f94..704c65467e0 100644 --- a/qa/qa/runtime/namespace.rb +++ b/qa/qa/runtime/namespace.rb @@ -8,7 +8,7 @@ module QA end def name - "qa-test-#{time.strftime('%Y-%m-%d-%H-%M-%S')}" + Runtime::Env.namespace_name || "qa-test-#{time.strftime('%Y-%m-%d-%H-%M-%S')}" end def path diff --git a/qa/qa/runtime/path.rb b/qa/qa/runtime/path.rb new file mode 100644 index 00000000000..3169c5dd743 --- /dev/null +++ b/qa/qa/runtime/path.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Runtime + module Path + extend self + + def qa_root + ::File.expand_path('../../', __dir__) + end + end + end +end diff --git a/qa/qa/scenario/test/integration/instance_saml.rb b/qa/qa/scenario/test/integration/instance_saml.rb new file mode 100644 index 00000000000..0697d0c2a0e --- /dev/null +++ b/qa/qa/scenario/test/integration/instance_saml.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class InstanceSAML < Test::Instance::All + tags :instance_saml + end + end + end + end +end diff --git a/qa/qa/scenario/test/integration/ldap_no_tls.rb b/qa/qa/scenario/test/integration/ldap_no_tls.rb new file mode 100644 index 00000000000..bbf4c847f33 --- /dev/null +++ b/qa/qa/scenario/test/integration/ldap_no_tls.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module QA + module Scenario + module Test + module Integration + class LDAPNoTLS < Test::Instance::All + tags :ldap_no_tls + end + end + end + end +end diff --git a/qa/qa/scenario/test/integration/ldap.rb b/qa/qa/scenario/test/integration/ldap_tls.rb index 769fa389785..2a767e57bc6 100644 --- a/qa/qa/scenario/test/integration/ldap.rb +++ b/qa/qa/scenario/test/integration/ldap_tls.rb @@ -1,9 +1,11 @@ +# frozen_string_literal: true + module QA module Scenario module Test module Integration - class LDAP < Test::Instance::All - tags :ldap + class LDAPTLS < Test::Instance::All + tags :ldap_tls end end end diff --git a/qa/qa/service/kubernetes_cluster.rb b/qa/qa/service/kubernetes_cluster.rb index abd9d53554f..c5f12255d72 100644 --- a/qa/qa/service/kubernetes_cluster.rb +++ b/qa/qa/service/kubernetes_cluster.rb @@ -1,12 +1,17 @@ require 'securerandom' require 'mkmf' +require 'pathname' module QA module Service class KubernetesCluster include Service::Shellout - attr_reader :api_url, :ca_certificate, :token + attr_reader :api_url, :ca_certificate, :token, :rbac + + def initialize(rbac: false) + @rbac = rbac + end def cluster_name @cluster_name ||= "qa-cluster-#{SecureRandom.hex(4)}-#{Time.now.utc.strftime("%Y%m%d%H%M%S")}" @@ -19,7 +24,8 @@ module QA shell <<~CMD.tr("\n", ' ') gcloud container clusters create #{cluster_name} - --enable-legacy-authorization + #{auth_options} + --enable-basic-auth --zone #{Runtime::Env.gcloud_zone} && gcloud container clusters get-credentials @@ -28,8 +34,30 @@ module QA CMD @api_url = `kubectl config view --minify -o jsonpath='{.clusters[].cluster.server}'` - @ca_certificate = Base64.decode64(`kubectl get secrets -o jsonpath="{.items[0].data['ca\\.crt']}"`) - @token = Base64.decode64(`kubectl get secrets -o jsonpath='{.items[0].data.token}'`) + + @admin_user = "#{cluster_name}-admin" + master_auth = JSON.parse(`gcloud container clusters describe #{cluster_name} --zone #{Runtime::Env.gcloud_zone} --format 'json(masterAuth.username, masterAuth.password)'`) + shell <<~CMD.tr("\n", ' ') + kubectl config set-credentials #{@admin_user} + --username #{master_auth['masterAuth']['username']} + --password #{master_auth['masterAuth']['password']} + CMD + + if rbac + create_service_account + + secrets = JSON.parse(`kubectl get secrets -o json`) + gitlab_account = secrets['items'].find do |item| + item['metadata']['annotations']['kubernetes.io/service-account.name'] == 'gitlab-account' + end + + @ca_certificate = Base64.decode64(gitlab_account['data']['ca.crt']) + @token = Base64.decode64(gitlab_account['data']['token']) + else + @ca_certificate = Base64.decode64(`kubectl get secrets -o jsonpath="{.items[0].data['ca\\.crt']}"`) + @token = Base64.decode64(`kubectl get secrets -o jsonpath='{.items[0].data.token}'`) + end + self end @@ -44,6 +72,42 @@ module QA private + def create_service_account + shell('kubectl create -f -', stdin_data: service_account) + shell("kubectl --user #{@admin_user} create -f -", stdin_data: service_account_role_binding) + end + + def service_account + <<~YAML + apiVersion: v1 + kind: ServiceAccount + metadata: + name: gitlab-account + namespace: default + YAML + end + + def service_account_role_binding + <<~YAML + kind: ClusterRoleBinding + apiVersion: rbac.authorization.k8s.io/v1 + metadata: + name: gitlab-account-binding + subjects: + - kind: ServiceAccount + name: gitlab-account + namespace: default + roleRef: + kind: ClusterRole + name: cluster-admin + apiGroup: rbac.authorization.k8s.io + YAML + end + + def auth_options + "--enable-legacy-authorization" unless rbac + end + def validate_dependencies find_executable('gcloud') || raise("You must first install `gcloud` executable to run these tests.") find_executable('kubectl') || raise("You must first install `kubectl` executable to run these tests.") diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb index 1ca9504bb33..43dc0851571 100644 --- a/qa/qa/service/shellout.rb +++ b/qa/qa/service/shellout.rb @@ -11,10 +11,12 @@ module QA # TODO, make it possible to use generic QA framework classes # as a library - gitlab-org/gitlab-qa#94 # - def shell(command) + def shell(command, stdin_data: nil) puts "Executing `#{command}`" - Open3.popen2e(*command) do |_in, out, wait| + Open3.popen2e(*command) do |stdin, out, wait| + stdin.puts(stdin_data) if stdin_data + stdin.close if stdin_data out.each { |line| puts line } if wait.value.exited? && wait.value.exitstatus.nonzero? diff --git a/qa/qa/specs/features/api/1_manage/users_spec.rb b/qa/qa/specs/features/api/1_manage/users_spec.rb index 3e3c9e859aa..ba1ba204d24 100644 --- a/qa/qa/specs/features/api/1_manage/users_spec.rb +++ b/qa/qa/specs/features/api/1_manage/users_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage do + context 'Manage' do describe 'Users API' do before(:context) do @api_client = Runtime::API::Client.new(:gitlab) diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb index 1c7da930567..dae2a9e0236 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_spec.rb @@ -1,5 +1,5 @@ module QA - context :manage, :smoke do + context 'Manage', :smoke do describe 'basic user login' do it 'user logs in using basic credentials' do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -8,7 +8,7 @@ module QA # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. # - Page::Menu::Main.perform do |menu| + Page::Main::Menu.perform do |menu| expect(menu).to have_personal_area end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb index c296296def6..a397df03bd2 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_gitlab_via_ldap_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage, :orchestrated, :ldap do + context 'Manage', :orchestrated, :ldap_no_tls, :ldap_tls do describe 'LDAP login' do it 'user logs into GitLab using LDAP credentials' do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -10,7 +10,7 @@ module QA # TODO, since `Signed in successfully` message was removed # this is the only way to tell if user is signed in correctly. # - Page::Menu::Main.perform do |menu| + Page::Main::Menu.perform do |menu| expect(menu).to have_personal_area end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb index 6eda2c750d4..b1d641b507f 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_into_mattermost_via_gitlab_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :manage, :orchestrated, :mattermost do + context 'Manage', :orchestrated, :mattermost do describe 'Mattermost login' do it 'user logs into Mattermost using GitLab OAuth' do Runtime::Browser.visit(:gitlab, Page::Main::Login) do diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb new file mode 100644 index 00000000000..87f0e9030d2 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_instance_wide_saml_sso_spec.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module QA + context 'Manage', :orchestrated, :instance_saml do + describe 'Instance wide SAML SSO' do + it 'User logs in to gitlab with SAML SSO' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + + Page::Main::Login.act { sign_in_with_saml } + + Vendor::SAMLIdp::Page::Login.act { login } + + expect(page).to have_content('Welcome to GitLab') + end + 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 new file mode 100644 index 00000000000..185837edacf --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module QA + shared_examples 'registration and login' do + it 'user registers and logs in' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + + Resource::User.fabricate_via_browser_ui! + + # TODO, since `Signed in successfully` message was removed + # this is the only way to tell if user is signed in correctly. + # + Page::Main::Menu.perform do |menu| + expect(menu).to have_personal_area + end + end + end + + context 'Manage', :skip_signup_disabled do + describe 'standard' do + it_behaves_like 'registration and login' + end + end + + context 'Manage', :orchestrated, :ldap_no_tls, :skip_signup_disabled do + describe 'while LDAP is enabled' do + it_behaves_like 'registration and login' + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb new file mode 100644 index 00000000000..4070a225260 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + context 'Manage' do + describe 'Add project member' do + it 'user adds project member' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) + + project = Resource::Project.fabricate! do |resource| + resource.name = 'add-member-project' + end + project.visit! + + Page::Project::Menu.perform(&:click_members_settings) + Page::Project::Settings::Members.perform do |page| + page.add_member(user.username) + end + + expect(page).to have_content("#{user.name} @#{user.username} Given access") + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_spec.rb index bb1f3ab26d1..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 @@ -1,23 +1,21 @@ # frozen_string_literal: true module QA - context :manage, :smoke do + context 'Manage', :smoke do describe 'Project creation' do it 'user creates a new project' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - created_project = Factory::Resource::Project.fabricate! do |project| + created_project = Resource::Project.fabricate_via_browser_ui! do |project| project.name = 'awesome-project' project.description = 'create awesome project test' end - expect(created_project.name).to match /^awesome-project-\h{16}$/ - + expect(page).to have_content(created_project.name) expect(page).to have_content( /Project \S?awesome-project\S+ was successfully created/ ) - expect(page).to have_content('create awesome project test') expect(page).to have_content('The repository for this project is empty') end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/import_github_repo_spec.rb index 2ef8de61441..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 @@ -1,10 +1,10 @@ # frozen_string_literal: true module QA - context :manage, :orchestrated, :github do + context 'Manage', :orchestrated, :github do describe 'Project import from GitHub' do let(:imported_project) do - Factory::Resource::ProjectImportedFromGithub.fabricate! do |project| + 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' @@ -27,7 +27,7 @@ module QA imported_project # import the project - Page::Menu::Main.act { go_to_projects } + Page::Main::Menu.act { go_to_projects } Page::Dashboard::Projects.perform do |dashboard| dashboard.go_to_project(imported_project.name) end @@ -48,7 +48,7 @@ module QA end def verify_issues_import - Page::Menu::Side.act { click_issues } + Page::Project::Menu.act { click_issues } expect(page).to have_content('This is a sample issue') click_link 'This is a sample issue' @@ -66,7 +66,7 @@ module QA end def verify_merge_requests_import - Page::Menu::Side.act { click_merge_requests } + Page::Project::Menu.act { click_merge_requests } expect(page).to have_content('Improve README.md') click_link 'Improve README.md' @@ -101,7 +101,7 @@ module QA end def verify_wiki_import - Page::Menu::Side.act { click_wiki } + Page::Project::Menu.act { click_wiki } expect(page).to have_content('Welcome to the test-project wiki!') end 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 34bb6f1c197..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 @@ -1,19 +1,19 @@ # frozen_string_literal: true module QA - context :manage do + context 'Manage' do describe 'Project activity' do it 'user creates an event in the activity page upon Git push' do Runtime::Browser.visit(:gitlab, Page::Main::Login) 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' end - Page::Menu::Side.act { go_to_activity } + Page::Project::Menu.act { go_to_activity } Page::Project::Activity.act { go_to_push_events } 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 542f532a629..7145b950b6c 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/create_issue_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :plan, :smoke do + context 'Plan', :smoke do describe 'Issue creation' do let(:issue_title) { 'issue title' } @@ -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 @@ -17,7 +17,7 @@ module QA it 'user creates an issue' do create_issue - Page::Menu::Side.act { click_issues } + Page::Project::Menu.act { click_issues } expect(page).to have_content(issue_title) end @@ -31,6 +31,7 @@ module QA create_issue Page::Project::Issue::Show.perform do |show| + show.select_all_activities_filter show.comment('See attached banana for scale', attachment: file_to_attach) show.refresh 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 new file mode 100644 index 00000000000..ac34f72bb8f --- /dev/null +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/filter_issue_comments_spec.rb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +module QA + context 'Plan' do + describe 'filter issue comments activities' do + let(:issue_title) { 'issue title' } + + it 'user filters comments and activites in an issue' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + Resource::Issue.fabricate! do |issue| + issue.title = issue_title + end + + expect(page).to have_content(issue_title) + + Page::Project::Issue::Show.perform do |show_page| + show_page.select_comments_only_filter + show_page.comment('/confidential') + show_page.comment('My own comment') + + expect(show_page).not_to have_content("made the issue confidential") + expect(show_page).to have_content("My own comment") + + show_page.select_all_activities_filter + + expect(show_page).to have_content("made the issue confidential") + expect(show_page).to have_content("My own comment") + + show_page.select_history_only_filter + + expect(show_page).to have_content("made the issue confidential") + expect(show_page).not_to have_content("My own comment") + end + end + end + end +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 bcf55a02a61..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 @@ -1,31 +1,41 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request creation' do it 'user creates a new merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) 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 - Factory::Resource::MergeRequest.fabricate! do |merge_request| + new_label = Resource::Label.fabricate! do |label| + label.project = current_project + label.title = 'qa-mr-test-label' + label.description = 'Merge Request label' + end + + Resource::MergeRequest.fabricate! do |merge_request| merge_request.title = 'This is a merge request with a milestone' merge_request.description = 'Great feature with milestone' merge_request.project = current_project merge_request.milestone = current_milestone + merge_request.labels.push(new_label) end - expect(page).to have_content('This is a merge request with a milestone') - expect(page).to have_content('Great feature with milestone') - expect(page).to have_content(/Opened [\w\s]+ ago/) + Page::MergeRequest::Show.perform do |merge_request| + expect(merge_request).to have_content('This is a merge request with a milestone') + expect(merge_request).to have_content('Great feature with milestone') + expect(merge_request).to have_content(/Opened [\w\s]+ ago/) + expect(merge_request).to have_label(new_label.title) + end Page::Issuable::Sidebar.perform do |sidebar| expect(sidebar).to have_milestone(current_milestone.title) @@ -39,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 407a15800ab..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 @@ -1,17 +1,17 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request creation from fork' do it 'user forks a project, submits a merge request and maintainer merges it' do Runtime::Browser.visit(:gitlab, Page::Main::Login) 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 - Page::Menu::Main.perform { |main| main.sign_out } + Page::Main::Menu.perform { |main| main.sign_out } Page::Main::Login.perform { |login| login.sign_in_using_credentials } merge_request.visit! 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 ddcbc94b1b1..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 @@ -1,25 +1,26 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request rebasing' do it 'user rebases source branch of merge request' do Runtime::Browser.visit(:gitlab, Page::Main::Login) 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! - Page::Menu::Side.act { go_to_settings } + 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 b5b8855a35d..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 @@ -1,22 +1,22 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Merge request squashing' do it 'user squashes commits while merging' do Runtime::Browser.visit(:gitlab, Page::Main::Login) 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 @@ -25,6 +25,7 @@ module QA push.file_content = "Test with unicode characters ❤✓€❄" end + Page::Project::Show.perform(&:wait_for_push) merge_request.visit! expect(page).to have_text('to be squashed') 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 new file mode 100644 index 00000000000..297485dd81e --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'File templates' do + include Runtime::Fixtures + + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + end + + before(:all) do + login + + @project = Resource::Project.fabricate! do |project| + project.name = 'file-template-project' + project.description = 'Add file templates via the Files view' + end + + Page::Main::Menu.act { sign_out } + end + + templates = [ + { + file_name: '.gitignore', + name: 'Android', + api_path: 'gitignores', + api_key: 'Android' + }, + { + file_name: '.gitlab-ci.yml', + name: 'Julia', + api_path: 'gitlab_ci_ymls', + api_key: 'Julia' + }, + { + file_name: 'Dockerfile', + name: 'Python', + api_path: 'dockerfiles', + api_key: 'Python' + }, + { + file_name: 'LICENSE', + name: 'Mozilla Public License 2.0', + api_path: 'licenses', + api_key: 'mpl-2.0' + } + ] + + templates.each do |template| + it "user adds #{template[:file_name]} via file template #{template[:name]}" do + content = fetch_template_from_api(template[:api_path], template[:api_key]) + + login + @project.visit! + + Page::Project::Show.act { create_new_file! } + Page::File::Form.perform do |page| + page.select_template template[:file_name], template[:name] + end + + expect(page).to have_content('Template applied') + expect(page).to have_button('Undo') + expect(page).to have_content(content[0..100]) + + Page::File::Form.perform(&:commit_changes) + + expect(page).to have_content('The file has been successfully created.') + expect(page).to have_content(template[:file_name]) + expect(page).to have_content('Add new file') + expect(page).to have_content(content[0..100]) + end + end + end + end +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 84f663c4866..ff879fdeb16 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_ssh_key_spec.rb @@ -1,23 +1,23 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'SSH keys support' do let(:key_title) { "key for ssh tests #{Time.now.to_f}" } - it 'user adds and then removes an SSH key' do + it 'user adds and then removes an SSH key', :smoke do 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 expect(page).to have_content("Title: #{key_title}") expect(page).to have_content(key.fingerprint) - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { click_ssh_keys } + Page::Main::Menu.act { go_to_profile_settings } + Page::Profile::Menu.act { click_ssh_keys } Page::Profile::SSHKeys.perform do |ssh_keys| ssh_keys.remove_key(key_title) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/clone_spec.rb index 0dcdc6639d1..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 @@ -1,8 +1,8 @@ # frozen_string_literal: true module QA - context :create do - describe 'Git clone over HTTP', :ldap do + context 'Create' do + describe 'Git clone over HTTP', :ldap_no_tls do let(:location) do Page::Project::Show.act do choose_repository_clone_http @@ -14,10 +14,11 @@ module QA Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::Project.fabricate! do |scenario| + project = Resource::Project.fabricate! do |scenario| scenario.name = 'project-with-code' scenario.description = 'project for git clone tests' end + project.visit! Git::Repository.perform do |repository| repository.uri = location.uri diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/create_edit_delete_file_via_web_spec.rb index 82d635065a0..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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Files management' do it 'user creates, edits and deletes a file via the Web' do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -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/protocol_v2_push_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb new file mode 100644 index 00000000000..43894372cf5 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_http_spec.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Push over HTTP using Git protocol version 2', :requires_git_protocol_v2 do + it 'user pushes to the repository' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + # Create a project to push to + project = Resource::Project.fabricate! do |project| + project.name = 'git-protocol-project' + end + + file_name = 'README.md' + file_content = 'Test Git protocol v2' + git_protocol = '2' + git_protocol_reported = nil + + # Use Git to clone the project, push a file to it, and then check the + # supported Git protocol + Git::Repository.perform do |repository| + username = 'GitLab QA' + email = 'root@gitlab.com' + + repository.uri = project.repository_http_location.uri + repository.use_default_credentials + repository.clone + repository.configure_identity(username, email) + + git_protocol_reported = repository.push_with_git_protocol( + git_protocol, + file_name, + file_content) + end + + project.visit! + Page::Project::Show.perform(&:wait_for_push) + + # Check that the push worked + expect(page).to have_content(file_name) + expect(page).to have_content(file_content) + + # And check that the correct Git protocol was used + expect(git_protocol_reported).to eq(git_protocol) + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb new file mode 100644 index 00000000000..135925c007f --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/protocol_v2_push_ssh_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Push over SSH using Git protocol version 2', :requires_git_protocol_v2 do + # Note: If you run this test against GDK make sure you've enabled sshd and + # enabled setting the Git protocol by adding `AcceptEnv GIT_PROTOCOL` to + # `sshd_config` + # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md + + let(:key_title) { "key for ssh tests #{Time.now.to_f}" } + let(:ssh_key) do + Resource::SSHKey.fabricate! do |resource| + resource.title = key_title + end + end + + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + end + + around do |example| + # Create an SSH key to be used with Git + login + ssh_key + + example.run + + # Remove the SSH key + login + Page::Main::Menu.perform(&:go_to_profile_settings) + Page::Profile::Menu.perform(&:click_ssh_keys) + Page::Profile::SSHKeys.perform do |ssh_keys| + ssh_keys.remove_key(key_title) + end + end + + it 'user pushes to the repository' do + # Create a project to push to + project = Resource::Project.fabricate! do |project| + project.name = 'git-protocol-project' + end + + file_name = 'README.md' + file_content = 'Test Git protocol v2' + git_protocol = '2' + git_protocol_reported = nil + + # Use Git to clone the project, push a file to it, and then check the + # supported Git protocol + Git::Repository.perform do |repository| + username = 'GitLab QA' + email = 'root@gitlab.com' + + repository.uri = project.repository_ssh_location.uri + + begin + repository.use_ssh_key(ssh_key) + repository.clone + repository.configure_identity(username, email) + + git_protocol_reported = repository.push_with_git_protocol( + git_protocol, + file_name, + file_content) + ensure + repository.delete_ssh_key + end + end + + project.visit! + Page::Project::Show.perform(&:wait_for_push) + + # Check that the push worked + expect(page).to have_content(file_name) + expect(page).to have_content(file_content) + + # And check that the correct Git protocol was used + expect(git_protocol_reported).to eq(git_protocol) + end + end + end +end 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 new file mode 100644 index 00000000000..a63b7dce8d6 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_http_private_token_spec.rb @@ -0,0 +1,32 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Git push over HTTP', :ldap_no_tls do + it 'user using a personal access token pushes code to the repository' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.perform(&:sign_in_using_credentials) + + access_token = Resource::PersonalAccessToken.fabricate!.access_token + + user = Resource::User.new.tap do |user| + user.username = Runtime::User.username + user.password = access_token + end + + push = Resource::Repository::ProjectPush.fabricate! do |push| + push.user = user + push.file_name = 'README.md' + push.file_content = '# This is a test project' + push.commit_message = 'Add README.md' + end + + push.project.visit! + Page::Project::Show.perform(&:wait_for_push) + + expect(page).to have_content('README.md') + expect(page).to have_content('This is a test project') + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_http_spec.rb index bf32569b6cb..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 @@ -1,13 +1,13 @@ # frozen_string_literal: true module QA - context :create do - describe 'Git push over HTTP', :ldap do + context 'Create' do + describe 'Git push over HTTP', :ldap_no_tls do it 'user pushes code to the repository' do 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 b2da685c477..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 @@ -1,12 +1,12 @@ # frozen_string_literal: true module QA - context :create do - describe 'Protected branch support', :ldap do + context 'Create' do + describe 'Protected branch support', :ldap_no_tls do 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 7c989bfd8cc..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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'SSH key support' do # Note: If you run this test against GDK make sure you've enabled sshd # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md @@ -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' @@ -28,8 +28,8 @@ module QA expect(page).to have_content('README.md') expect(page).to have_content('Test Use SSH Key') - Page::Menu::Main.act { go_to_profile_settings } - Page::Menu::Profile.act { click_ssh_keys } + Page::Main::Menu.act { go_to_profile_settings } + Page::Profile::Menu.act { click_ssh_keys } Page::Profile::SSHKeys.perform do |ssh_keys| ssh_keys.remove_key(key_title) 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 new file mode 100644 index 00000000000..e7374377104 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -0,0 +1,88 @@ +# frozen_string_literal: true + +module QA + context 'Create' do + describe 'Web IDE file templates' do + include Runtime::Fixtures + + def login + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + end + + before(:all) do + login + + @project = Resource::Project.fabricate! do |project| + project.name = 'file-template-project' + project.description = 'Add file templates via the Web IDE' + end + @project.visit! + + # Add a file via the regular Files view because the Web IDE isn't + # available unless there is a file present + Page::Project::Show.act { create_new_file! } + Page::File::Form.perform do |page| + page.add_name('dummy') + page.add_content('Enable the Web IDE') + page.commit_changes + end + + Page::Main::Menu.act { sign_out } + end + + templates = [ + { + file_name: '.gitignore', + name: 'Android', + api_path: 'gitignores', + api_key: 'Android' + }, + { + file_name: '.gitlab-ci.yml', + name: 'Julia', + api_path: 'gitlab_ci_ymls', + api_key: 'Julia' + }, + { + file_name: 'Dockerfile', + name: 'Python', + api_path: 'dockerfiles', + api_key: 'Python' + }, + { + file_name: 'LICENSE', + name: 'Mozilla Public License 2.0', + api_path: 'licenses', + api_key: 'mpl-2.0' + } + ] + + templates.each do |template| + it "user adds #{template[:file_name]} via file template #{template[:name]}" do + content = fetch_template_from_api(template[:api_path], template[:api_key]) + + login + @project.visit! + + Page::Project::Show.act { open_web_ide! } + Page::Project::WebIDE::Edit.perform do |page| + page.create_new_file_from_template template[:file_name], template[:name] + + expect(page.has_file?(template[:file_name])).to be_truthy + end + + expect(page).to have_button('Undo') + expect(page).to have_content(content[0..100]) + + Page::Project::WebIDE::Edit.perform do |page| + page.commit_changes + end + + expect(page).to have_content(template[:file_name]) + expect(page).to have_content(content[0..100]) + end + end + end + end +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 8009b9e8609..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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :create do + context 'Create' do describe 'Wiki management' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -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,13 +34,13 @@ 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' push.commit_message = 'Update Home.md' end - Page::Menu::Side.act { click_wiki } + Page::Project::Menu.act { click_wiki } expect(page).to have_content('My Third Wiki Content') end diff --git a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb index 08a87df5837..0837b720df1 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/secret_variable/add_secret_variable_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/ci_variable/add_ci_variable_spec.rb @@ -1,25 +1,25 @@ # frozen_string_literal: true module QA - context :verify do - describe 'Secret variable support' do - it 'user adds a secret variable' do + context 'Verify' do + describe 'CI variable support' do + it 'user adds a CI variable' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Factory::Resource::SecretVariable.fabricate! do |resource| + Resource::CiVariable.fabricate! do |resource| resource.key = 'VARIABLE_KEY' - resource.value = 'some secret variable' + resource.value = 'some CI variable' end Page::Project::Settings::CICD.perform do |settings| - settings.expand_secret_variables do |page| + settings.expand_ci_variables do |page| expect(page).to have_field(with: 'VARIABLE_KEY') - expect(page).not_to have_field(with: 'some secret variable') + expect(page).not_to have_field(with: 'some CI variable') page.reveal_variables - expect(page).to have_field(with: 'some secret variable') + expect(page).to have_field(with: 'some CI variable') end end 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 cdfe9b90e15..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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :verify, :orchestrated, :docker do + context 'Verify', :orchestrated, :docker do describe 'Pipeline creation and processing' do let(:executor) { "qa-runner-#{Time.now.to_i}" } @@ -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' @@ -64,7 +64,7 @@ module QA expect(page).to have_content('Add .gitlab-ci.yml') - Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Menu.act { click_ci_cd_pipelines } expect(page).to have_content('All 1') expect(page).to have_content('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 8d83a20f5bf..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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :verify, :docker do + context 'Verify', :docker do describe 'Runner registration' do let(:executor) { "qa-runner-#{Time.now.to_i}" } @@ -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 17dfa887434..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 @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - context :release do + context 'Release' do describe 'Deploy key creation' do it 'user adds a deploy key' do Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -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 8352d13b06d..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 @@ -3,7 +3,7 @@ require 'digest/sha1' module QA - context :release, :docker do + context 'Release', :docker do describe 'Git clone using a deploy key' do def login Runtime::Browser.visit(:gitlab, Page::Main::Login) @@ -15,20 +15,20 @@ 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] resource.image = 'gitlab/gitlab-runner:ubuntu' end - Page::Menu::Main.act { sign_out } + Page::Main::Menu.act { sign_out } end after(:all) do @@ -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::SecretVariable.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' @@ -90,7 +90,7 @@ module QA sha1sum = Digest::SHA1.hexdigest(gitlab_ci) Page::Project::Show.act { wait_for_push } - Page::Menu::Side.act { click_ci_cd_pipelines } + Page::Project::Menu.act { click_ci_cd_pipelines } Page::Project::Pipeline::Index.act { go_to_latest_pipeline } Page::Project::Pipeline::Show.act { go_to_first_job } 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 new file mode 100644 index 00000000000..9f34e4218c1 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_token/add_deploy_token_spec.rb @@ -0,0 +1,23 @@ +# frozen_string_literal: true + +module QA + context 'Release' do + describe 'Deploy token creation' do + it 'user adds a deploy token' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } + + deploy_token_name = 'deploy token name' + deploy_token_expires_at = Date.today + 7 # 1 Week from now + + deploy_token = Resource::DeployToken.fabricate! do |resource| + resource.name = deploy_token_name + resource.expires_at = deploy_token_expires_at + end + + expect(deploy_token.username.length).to be > 0 + expect(deploy_token.password.length).to be > 0 + end + end + end +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 844cc1236c7..b0c277a48c3 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/create_project_with_auto_devops_spec.rb @@ -3,65 +3,81 @@ require 'pathname' module QA - context :configure, :orchestrated, :kubernetes do + context 'Configure', :orchestrated, :kubernetes do describe 'Auto DevOps support' do after do @cluster&.remove! end - it 'user creates a new project and runs auto devops' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.act { sign_in_using_credentials } + [true, false].each do |rbac| + context "when rbac is #{rbac ? 'enabled' : 'disabled'}" do + it 'user creates a new project and runs auto devops' do + Runtime::Browser.visit(:gitlab, Page::Main::Login) + Page::Main::Login.act { sign_in_using_credentials } - project = Factory::Resource::Project.fabricate! do |p| - p.name = 'project-with-autodevops' - p.description = 'Project with Auto Devops' - end + project = Resource::Project.fabricate! do |p| + p.name = Runtime::Env.auto_devops_project_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::SecretVariable.fabricate! do |resource| - resource.project = project - resource.key = 'CODE_QUALITY_DISABLED' - resource.value = '1' - end + # Disable code_quality check in Auto DevOps pipeline as it takes + # too long and times out the test + 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| - push.project = project - push.directory = Pathname - .new(__dir__) - .join('../../../../../fixtures/auto_devops_rack') - push.commit_message = 'Create Auto DevOps compatible rack application' - end + # Create Auto Devops compatible repo + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.directory = Pathname + .new(__dir__) + .join('../../../../../fixtures/auto_devops_rack') + push.commit_message = 'Create Auto DevOps compatible rack application' + end - Page::Project::Show.act { wait_for_push } + Page::Project::Show.act { wait_for_push } - # Create and connect K8s cluster - @cluster = Service::KubernetesCluster.new.create! - kubernetes_cluster = Factory::Resource::KubernetesCluster.fabricate! do |cluster| - cluster.project = project - cluster.cluster = @cluster - cluster.install_helm_tiller = true - cluster.install_ingress = true - cluster.install_prometheus = true - cluster.install_runner = true - end + # Create and connect K8s cluster + @cluster = Service::KubernetesCluster.new(rbac: rbac).create! + kubernetes_cluster = Resource::KubernetesCluster.fabricate! do |cluster| + cluster.project = project + cluster.cluster = @cluster + cluster.install_helm_tiller = true + cluster.install_ingress = true + cluster.install_prometheus = true + cluster.install_runner = true + end + kubernetes_cluster.populate(:ingress_ip) - project.visit! - Page::Menu::Side.act { click_ci_cd_settings } - Page::Project::Settings::CICD.perform do |p| - p.enable_auto_devops_with_domain("#{kubernetes_cluster.ingress_ip}.nip.io") - end + project.visit! + Page::Project::Menu.act { click_ci_cd_settings } + Page::Project::Settings::CICD.perform do |p| + p.enable_auto_devops_with_domain( + "#{kubernetes_cluster.ingress_ip}.nip.io") + end + + project.visit! + Page::Project::Menu.act { click_ci_cd_pipelines } + Page::Project::Pipeline::Index.act { go_to_latest_pipeline } - project.visit! - Page::Menu::Side.act { click_ci_cd_pipelines } - Page::Project::Pipeline::Index.act { go_to_latest_pipeline } + Page::Project::Pipeline::Show.perform do |pipeline| + expect(pipeline).to have_build('build', status: :success, wait: 600) + expect(pipeline).to have_build('test', status: :success, wait: 600) + expect(pipeline).to have_build('production', status: :success, wait: 1200) + end - Page::Project::Pipeline::Show.perform do |pipeline| - expect(pipeline).to have_build('build', status: :success, wait: 600) - expect(pipeline).to have_build('test', status: :success, wait: 600) - expect(pipeline).to have_build('production', status: :success, wait: 1200) + Page::Project::Menu.act { click_operations_environments } + Page::Project::Operations::Environments::Index.perform do |index| + index.go_to_environment('production') + end + Page::Project::Operations::Environments::Show.perform do |show| + show.view_deployment do + expect(page).to have_content('Hello World!') + end + end + end end end end diff --git a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb index 6ffdc55538a..7096864e011 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/mattermost/create_group_with_mattermost_team_spec.rb @@ -1,12 +1,12 @@ # frozen_string_literal: true module QA - context :configure, :orchestrated, :mattermost do + context 'Configure', :orchestrated, :mattermost do describe 'Mattermost support' do it 'user creates a group with a mattermost team' do Runtime::Browser.visit(:gitlab, Page::Main::Login) Page::Main::Login.act { sign_in_using_credentials } - Page::Menu::Main.act { go_to_groups } + Page::Main::Menu.act { go_to_groups } Page::Dashboard::Groups.perform do |page| page.go_to_new_group diff --git a/qa/qa/specs/features/sanity/framework_spec.rb b/qa/qa/specs/features/sanity/framework_spec.rb index ee9d068eb3a..aae0f0ade71 100644 --- a/qa/qa/specs/features/sanity/framework_spec.rb +++ b/qa/qa/specs/features/sanity/framework_spec.rb @@ -6,9 +6,7 @@ module QA it 'succeeds' do Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform do |main_login| - expect(main_login.sign_in_tab?).to be(true) - end + expect(page).to have_text('Open source software to collaborate on code') end end diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index fea0ef94df3..1bd8101c36d 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -23,6 +23,12 @@ module QA args.push(%w[--tag ~orchestrated]) unless (%w[-t --tag] & options).any? end + args.push(%w[--tag ~skip_signup_disabled]) if QA::Runtime::Env.signup_disabled? + + QA::Runtime::Env.supported_features.each_key do |key| + args.push(["--tag", "~requires_#{key}"]) unless QA::Runtime::Env.can_test? key + end + args.push(options) args.push(DEFAULT_TEST_PATH_ARGS) unless options.any? { |opt| opt =~ %r{/features/} } diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb new file mode 100644 index 00000000000..1107d43161e --- /dev/null +++ b/qa/qa/support/api.rb @@ -0,0 +1,28 @@ +module QA + module Support + module Api + def post(url, payload) + RestClient::Request.execute( + method: :post, + url: url, + payload: payload, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + e.response + end + + def get(url) + RestClient::Request.execute( + method: :get, + url: url, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + e.response + end + + def parse_body(response) + JSON.parse(response.body, symbolize_names: true) + end + end + end +end diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb new file mode 100644 index 00000000000..cf5cd3a79f8 --- /dev/null +++ b/qa/qa/support/page/logging.rb @@ -0,0 +1,100 @@ +# frozen_string_literal: true + +module QA + module Support + module Page + module Logging + def refresh + log("refreshing #{current_url}") + + super + end + + def wait(max: 60, time: 0.1, reload: true) + log("with wait: max #{max}; time #{time}; reload #{reload}") + now = Time.now + + element = super + + log("ended wait after #{Time.now - now} seconds") + + element + end + + def scroll_to(selector, text: nil) + msg = "scrolling to :#{selector}" + msg += " with text: #{text}" if text + log(msg) + + super + end + + def asset_exists?(url) + exists = super + + log("asset_exists? #{url} returned #{exists}") + + exists + end + + def find_element(name) + log("finding :#{name}") + + element = super + + log("found :#{name}") if element + + element + end + + def all_elements(name) + log("finding all :#{name}") + + elements = super + + log("found #{elements.size} :#{name}") if elements + + elements + end + + def click_element(name) + log("clicking :#{name}") + + super + end + + def fill_element(name, content) + masked_content = name.to_s.include?('password') ? '*****' : content + + log(%Q(filling :#{name} with "#{masked_content}")) + + super + end + + def has_element?(name) + found = super + + log("has_element? :#{name} returned #{found}") + + found + end + + def within_element(name) + log("within element :#{name}") + + element = super + + log("end within element :#{name}") + + element + end + + private + + def log(msg) + QA::Runtime::Logger.debug(msg) + end + end + end + end +end diff --git a/qa/qa/vendor/saml_idp/page/base.rb b/qa/qa/vendor/saml_idp/page/base.rb new file mode 100644 index 00000000000..286cb0a8cd8 --- /dev/null +++ b/qa/qa/vendor/saml_idp/page/base.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module QA + module Vendor + module SAMLIdp + module Page + class Base + include Capybara::DSL + include Scenario::Actable + end + end + end + end +end diff --git a/qa/qa/vendor/saml_idp/page/login.rb b/qa/qa/vendor/saml_idp/page/login.rb new file mode 100644 index 00000000000..9c1f9904a7a --- /dev/null +++ b/qa/qa/vendor/saml_idp/page/login.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'capybara/dsl' + +module QA + module Vendor + module SAMLIdp + module Page + class Login < Page::Base + def login + fill_in 'username', with: 'user1' + fill_in 'password', with: 'user1pass' + click_on 'Login' + end + 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 04e04886699..00000000000 --- a/qa/spec/factory/base_spec.rb +++ /dev/null @@ -1,132 +0,0 @@ -describe QA::Factory::Base do - let(:factory) { spy('factory') } - let(:product) { spy('product') } - - describe '.fabricate!' do - subject { Class.new(described_class) } - - before do - allow(QA::Factory::Product).to receive(:new).and_return(product) - allow(QA::Factory::Product).to receive(:populate!).and_return(product) - end - - it 'instantiates the factory and calls factory method' do - expect(subject).to receive(:new).and_return(factory) - - subject.fabricate!('something') - - expect(factory).to have_received(:fabricate!).with('something') - end - - it 'returns fabrication product' do - allow(subject).to receive(:new).and_return(factory) - - result = subject.fabricate!('something') - - expect(result).to eq product - end - - it 'yields factory before calling factory method' do - allow(subject).to receive(:new).and_return(factory) - - subject.fabricate! do |factory| - factory.something! - end - - expect(factory).to have_received(:something!).ordered - expect(factory).to have_received(:fabricate!).ordered - end - end - - describe '.dependency' do - let(:dependency) { spy('dependency') } - - before do - stub_const('Some::MyDependency', dependency) - end - - subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep do |factory| - factory.something! - end - end - end - - it 'appends a new dependency and accessors' do - expect(subject.dependencies).to be_one - end - - it 'defines dependency accessors' do - expect(subject.new).to respond_to :mydep, :mydep= - end - - describe 'dependencies fabrication' do - let(:dependency) { double('dependency') } - let(:instance) { spy('instance') } - - subject do - Class.new(described_class) do - dependency Some::MyDependency, as: :mydep - end - end - - before do - stub_const('Some::MyDependency', dependency) - - allow(subject).to receive(:new).and_return(instance) - allow(instance).to receive(:mydep).and_return(nil) - allow(QA::Factory::Product).to receive(:new) - allow(QA::Factory::Product).to receive(:populate!) - end - - it 'builds all dependencies first' do - expect(dependency).to receive(:fabricate!).once - - subject.fabricate! - end - end - end - - describe '.product' do - subject do - Class.new(described_class) do - def fabricate! - "any" - end - - # Defined only to be stubbed - def self.find_page - end - - product :token do - find_page.do_something_on_page! - 'resulting value' - end - end - end - - it 'appends new product attribute' do - expect(subject.attributes).to be_one - expect(subject.attributes).to have_key(:token) - end - - describe 'populating fabrication product with data' do - let(:page) { spy('page') } - - before do - allow(factory).to receive(:class).and_return(subject) - allow(QA::Factory::Product).to receive(:new).and_return(product) - allow(product).to receive(:page).and_return(page) - allow(subject).to receive(:find_page).and_return(page) - end - - it 'populates product after fabrication' do - subject.fabricate! - - expect(product.token).to eq 'resulting value' - expect(page).to have_received(:do_something_on_page!) - end - end - end -end diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb deleted file mode 100644 index 8aaa6665a18..00000000000 --- a/qa/spec/factory/dependency_spec.rb +++ /dev/null @@ -1,72 +0,0 @@ -describe QA::Factory::Dependency do - let(:dependency) { spy('dependency' ) } - let(:factory) { spy('factory') } - let(:block) { spy('block') } - - let(:signature) do - double('signature', factory: dependency, block: block) - end - - subject do - described_class.new(:mydep, factory, signature) - end - - describe '#overridden?' do - it 'returns true if factory has overridden dependency' do - allow(factory).to receive(:mydep).and_return('something') - - expect(subject).to be_overridden - end - - it 'returns false if dependency has not been overridden' do - allow(factory).to receive(:mydep).and_return(nil) - - expect(subject).not_to be_overridden - end - end - - describe '#build!' do - context 'when dependency has been overridden' do - before do - allow(subject).to receive(:overridden?).and_return(true) - end - - it 'does not fabricate dependency' do - subject.build! - - expect(dependency).not_to have_received(:fabricate!) - end - end - - context 'when dependency has not been overridden' do - before do - allow(subject).to receive(:overridden?).and_return(false) - end - - it 'fabricates dependency' do - subject.build! - - expect(dependency).to have_received(:fabricate!) - end - - it 'sets product in the factory' do - subject.build! - - expect(factory).to have_received(:mydep=).with(dependency) - end - - context 'when receives a caller factory as block argument' do - let(:dependency) { QA::Factory::Base } - - it 'calls given block with dependency factory and caller factory' do - allow_any_instance_of(QA::Factory::Base).to receive(:fabricate!).and_return(factory) - allow(QA::Factory::Product).to receive(:populate!).and_return(spy('any')) - - subject.build! - - expect(block).to have_received(:call).with(an_instance_of(QA::Factory::Base), factory) - end - end - end - end -end diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb deleted file mode 100644 index f245aabbf43..00000000000 --- a/qa/spec/factory/product_spec.rb +++ /dev/null @@ -1,39 +0,0 @@ -describe QA::Factory::Product do - let(:factory) do - QA::Factory::Base.new - end - - let(:attributes) do - { test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) } - end - - let(:product) { spy('product') } - - before do - allow(QA::Factory::Base).to receive(:attributes).and_return(attributes) - end - - describe '.populate!' do - it 'returns a fabrication product and define factory attributes as its methods' do - expect(described_class).to receive(:new).and_return(product) - - result = described_class.populate!(factory) do |instance| - instance.something = 'string' - end - - expect(result).to be product - expect(result.test).to eq('returned') - end - end - - describe '.visit!' do - it 'makes it possible to visit fabrication product' do - allow_any_instance_of(described_class) - .to receive(:current_url).and_return('some url') - allow_any_instance_of(described_class) - .to receive(:visit).and_return('visited some url') - - expect(subject.visit!).to eq 'visited some url' - end - end -end diff --git a/qa/spec/factory/resource/user_spec.rb b/qa/spec/factory/resource/user_spec.rb new file mode 100644 index 00000000000..820c506b715 --- /dev/null +++ b/qa/spec/factory/resource/user_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +describe QA::Resource::User do + describe "#fabricate_via_api!" do + Response = Struct.new(:code, :body) + + it 'fetches an existing user' do + existing_users = [ + { + id: '0', + name: 'name', + username: 'name', + web_url: '' + } + ] + users_response = Response.new('200', JSON.dump(existing_users)) + single_user_response = Response.new('200', JSON.dump(existing_users.first)) + + expect(subject).to receive(:api_get_from).with("/users?username=name").and_return(users_response) + expect(subject).to receive(:api_get_from).with("/users/0").and_return(single_user_response) + + subject.username = 'name' + subject.fabricate_via_api! + + expect(subject.api_response).to eq(existing_users.first) + end + + it 'tries to create a user if it does not exist' do + expect(subject).to receive(:api_get_from).with("/users?username=foo").and_return(Response.new('200', '[]')) + expect(subject).to receive(:api_post).and_return({ web_url: '' }) + + subject.username = 'foo' + subject.fabricate_via_api! + end + end +end diff --git a/qa/spec/git/repository_spec.rb b/qa/spec/git/repository_spec.rb index 53bff3bf0b3..faa154c78da 100644 --- a/qa/spec/git/repository_spec.rb +++ b/qa/spec/git/repository_spec.rb @@ -1,17 +1,18 @@ describe QA::Git::Repository do + include Support::StubENV + let(:repository) { described_class.new } before do + stub_env('GITLAB_USERNAME', 'root') cd_empty_temp_directory set_bad_uri repository.use_default_credentials end describe '#clone' do - it 'redacts credentials from the URI in output' do - output, _ = repository.clone - - expect(output).to include("fatal: unable to access 'http://****@foo/bar.git/'") + it 'is unable to resolve host' do + expect(repository.clone).to include("fatal: unable to access 'http://root@foo/bar.git/'") end end @@ -20,10 +21,38 @@ describe QA::Git::Repository do `git init` # need a repo to push from end - it 'redacts credentials from the URI in output' do - output, _ = repository.push_changes + it 'fails to push changes' do + expect(repository.push_changes).to include("error: failed to push some refs to 'http://root@foo/bar.git'") + end + end + + describe '#git_protocol=' do + [0, 1, 2].each do |version| + it "configures git to use protocol version #{version}" do + expect(repository).to receive(:run).with("git config protocol.version #{version}") + repository.git_protocol = version + end + end + + it 'raises an error if the version is unsupported' do + expect { repository.git_protocol = 'foo' }.to raise_error(ArgumentError, "Please specify the protocol you would like to use: 0, 1, or 2") + end + end + + describe '#fetch_supported_git_protocol' do + it "reports the detected version" do + expect(repository).to receive(:run).and_return("packet: git< version 2") + expect(repository.fetch_supported_git_protocol).to eq('2') + end + + it 'reports unknown if version is unknown' do + expect(repository).to receive(:run).and_return("packet: git< version -1") + expect(repository.fetch_supported_git_protocol).to eq('unknown') + end - expect(output).to include("error: failed to push some refs to 'http://****@foo/bar.git'") + it 'reports unknown if content does not identify a version' do + expect(repository).to receive(:run).and_return("foo") + expect(repository.fetch_supported_git_protocol).to eq('unknown') end end diff --git a/qa/spec/page/base_spec.rb b/qa/spec/page/base_spec.rb index 52daa9697ee..076a8087db5 100644 --- a/qa/spec/page/base_spec.rb +++ b/qa/spec/page/base_spec.rb @@ -9,12 +9,12 @@ describe QA::Page::Base do subject do Class.new(described_class) do view 'path/to/some/view.html.haml' do - element :something, 'string pattern' - element :something_else, /regexp pattern/ + element :something, 'string pattern' # rubocop:disable QA/ElementWithPattern + element :something_else, /regexp pattern/ # rubocop:disable QA/ElementWithPattern end view 'path/to/some/_partial.html.haml' do - element :another_element, 'string pattern' + element :another_element, 'string pattern' # rubocop:disable QA/ElementWithPattern end end end diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb new file mode 100644 index 00000000000..9d56353062b --- /dev/null +++ b/qa/spec/page/logging_spec.rb @@ -0,0 +1,94 @@ +# frozen_string_literal: true + +require 'capybara/dsl' + +describe QA::Support::Page::Logging do + include Support::StubENV + + let(:page) { double().as_null_object } + + before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + QA::Runtime::Logger.logger = logger + + allow(Capybara).to receive(:current_session).and_return(page) + allow(page).to receive(:current_url).and_return('http://current-url') + allow(page).to receive(:has_css?).with(any_args).and_return(true) + end + + subject do + Class.new(QA::Page::Base) do + prepend QA::Support::Page::Logging + end.new + end + + it 'logs refresh' do + expect { subject.refresh } + .to output(%r{refreshing http://current-url}).to_stdout_from_any_process + end + + it 'logs wait' do + expect { subject.wait(max: 0) {} } + .to output(/with wait/).to_stdout_from_any_process + expect { subject.wait(max: 0) {} } + .to output(/ended wait after .* seconds$/).to_stdout_from_any_process + end + + it 'logs scroll_to' do + expect { subject.scroll_to(:element) } + .to output(/scrolling to :element/).to_stdout_from_any_process + end + + it 'logs asset_exists?' do + expect { subject.asset_exists?('http://asset-url') } + .to output(%r{asset_exists\? http://asset-url returned false}).to_stdout_from_any_process + end + + it 'logs find_element' do + expect { subject.find_element(:element) } + .to output(/found :element/).to_stdout_from_any_process + end + + it 'logs click_element' do + expect { subject.click_element(:element) } + .to output(/clicking :element/).to_stdout_from_any_process + end + + it 'logs fill_element' do + expect { subject.fill_element(:element, 'foo') } + .to output(/filling :element with "foo"/).to_stdout_from_any_process + end + + it 'logs has_element?' do + expect { subject.has_element?(:element) } + .to output(/has_element\? :element returned true/).to_stdout_from_any_process + end + + it 'logs within_element' do + expect { subject.within_element(:element) } + .to output(/within element :element/).to_stdout_from_any_process + expect { subject.within_element(:element) } + .to output(/end within element :element/).to_stdout_from_any_process + end + + context 'all_elements' do + it 'logs the number of elements found' do + allow(page).to receive(:all).and_return([1, 2]) + + expect { subject.all_elements(:element) } + .to output(/finding all :element/).to_stdout_from_any_process + expect { subject.all_elements(:element) } + .to output(/found 2 :element/).to_stdout_from_any_process + end + + it 'logs 0 if no elements are found' do + allow(page).to receive(:all).and_return([]) + + expect { subject.all_elements(:element) } + .to output(/finding all :element/).to_stdout_from_any_process + expect { subject.all_elements(:element) } + .not_to output(/found 0 :elements/).to_stdout_from_any_process + end + end +end diff --git a/qa/spec/page/validator_spec.rb b/qa/spec/page/validator_spec.rb index 55957649904..0ae6e66d767 100644 --- a/qa/spec/page/validator_spec.rb +++ b/qa/spec/page/validator_spec.rb @@ -30,7 +30,7 @@ describe QA::Page::Validator do let(:view) { spy('view') } before do - allow(QA::Page::Admin::Settings::Main) + allow(QA::Page::Admin::Settings::Repository) .to receive(:views).and_return([view]) end diff --git a/qa/spec/page/view_spec.rb b/qa/spec/page/view_spec.rb index 34d2ff11447..d7b3ccd316d 100644 --- a/qa/spec/page/view_spec.rb +++ b/qa/spec/page/view_spec.rb @@ -8,8 +8,8 @@ describe QA::Page::View do describe '.evaluate' do it 'evaluates a block and returns a DSL object' do results = described_class.evaluate do - element :something, 'my pattern' - element :something_else, /another pattern/ + element :something + element :something_else end expect(results.elements.size).to eq 2 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 diff --git a/qa/spec/runtime/api/client_spec.rb b/qa/spec/runtime/api/client_spec.rb index d497d8839b8..975586b505f 100644 --- a/qa/spec/runtime/api/client_spec.rb +++ b/qa/spec/runtime/api/client_spec.rb @@ -13,18 +13,27 @@ describe QA::Runtime::API::Client do end end - describe '#get_personal_access_token' do - it 'returns specified token from env' do - stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + describe '#personal_access_token' do + context 'when QA::Runtime::Env.personal_access_token is present' do + before do + allow(QA::Runtime::Env).to receive(:personal_access_token).and_return('a_token') + end - expect(described_class.new.get_personal_access_token).to eq 'a_token' + it 'returns specified token from env' do + expect(described_class.new.personal_access_token).to eq 'a_token' + end end - it 'returns a created token' do - allow_any_instance_of(described_class) - .to receive(:create_personal_access_token).and_return('created_token') + context 'when QA::Runtime::Env.personal_access_token is nil' do + before do + allow(QA::Runtime::Env).to receive(:personal_access_token).and_return(nil) + end - expect(described_class.new.get_personal_access_token).to eq 'created_token' + it 'returns a created token' do + expect(subject).to receive(:create_personal_access_token).and_return('created_token') + + expect(subject.personal_access_token).to eq 'created_token' + end end end end diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb index 80e3149f32d..08233e3c1d6 100644 --- a/qa/spec/runtime/api/request_spec.rb +++ b/qa/spec/runtime/api/request_spec.rb @@ -1,17 +1,23 @@ describe QA::Runtime::API::Request do - include Support::StubENV + let(:client) { QA::Runtime::API::Client.new('http://example.com') } + let(:request) { described_class.new(client, '/users') } before do - stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + allow(client).to receive(:personal_access_token).and_return('a_token') end - let(:client) { QA::Runtime::API::Client.new('http://example.com') } - let(:request) { described_class.new(client, '/users') } - describe '#url' do - it 'returns the full api request url' do + it 'returns the full API request url' do expect(request.url).to eq 'http://example.com/api/v4/users?private_token=a_token' end + + context 'when oauth_access_token is passed in the query string' do + let(:request) { described_class.new(client, '/users', { oauth_access_token: 'foo' }) } + + it 'does not adds a private_token query string' do + expect(request.url).to eq 'http://example.com/api/v4/users?oauth_access_token=foo' + end + end end describe '#request_path' do diff --git a/qa/spec/runtime/api_request_spec.rb b/qa/spec/runtime/api_request_spec.rb deleted file mode 100644 index e69de29bb2d..00000000000 --- a/qa/spec/runtime/api_request_spec.rb +++ /dev/null diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb index d889d185a45..ded51d5bb7c 100644 --- a/qa/spec/runtime/env_spec.rb +++ b/qa/spec/runtime/env_spec.rb @@ -1,39 +1,66 @@ +# frozen_string_literal: true + describe QA::Runtime::Env do include Support::StubENV - describe '.chrome_headless?' do + shared_examples 'boolean method' do |**kwargs| + it_behaves_like 'boolean method with parameter', kwargs + end + + shared_examples 'boolean method with parameter' do |method:, param: nil, env_key:, default:| context 'when there is an env variable set' do it 'returns false when falsey values specified' do - stub_env('CHROME_HEADLESS', 'false') - expect(described_class.chrome_headless?).to be_falsey + stub_env(env_key, 'false') + expect(described_class.public_send(method, *param)).to be_falsey - stub_env('CHROME_HEADLESS', 'no') - expect(described_class.chrome_headless?).to be_falsey + stub_env(env_key, 'no') + expect(described_class.public_send(method, *param)).to be_falsey - stub_env('CHROME_HEADLESS', '0') - expect(described_class.chrome_headless?).to be_falsey + stub_env(env_key, '0') + expect(described_class.public_send(method, *param)).to be_falsey end it 'returns true when anything else specified' do - stub_env('CHROME_HEADLESS', 'true') - expect(described_class.chrome_headless?).to be_truthy + stub_env(env_key, 'true') + expect(described_class.public_send(method, *param)).to be_truthy - stub_env('CHROME_HEADLESS', '1') - expect(described_class.chrome_headless?).to be_truthy + stub_env(env_key, '1') + expect(described_class.public_send(method, *param)).to be_truthy - stub_env('CHROME_HEADLESS', 'anything') - expect(described_class.chrome_headless?).to be_truthy + stub_env(env_key, 'anything') + expect(described_class.public_send(method, *param)).to be_truthy end end context 'when there is no env variable set' do - it 'returns the default, true' do - stub_env('CHROME_HEADLESS', nil) - expect(described_class.chrome_headless?).to be_truthy + it "returns the default, #{default}" do + stub_env(env_key, nil) + expect(described_class.public_send(method, *param)).to be(default) end end end + describe '.signup_disabled?' do + it_behaves_like 'boolean method', + method: :signup_disabled?, + env_key: 'SIGNUP_DISABLED', + default: false + end + + describe '.debug?' do + it_behaves_like 'boolean method', + method: :debug?, + env_key: 'QA_DEBUG', + default: false + end + + describe '.chrome_headless?' do + it_behaves_like 'boolean method', + method: :chrome_headless?, + env_key: 'CHROME_HEADLESS', + default: true + end + describe '.running_in_ci?' do context 'when there is an env variable set' do it 'returns true if CI' do @@ -56,7 +83,54 @@ describe QA::Runtime::Env do end end + describe '.personal_access_token' do + around do |example| + described_class.instance_variable_set(:@personal_access_token, nil) + example.run + described_class.instance_variable_set(:@personal_access_token, nil) + end + + context 'when PERSONAL_ACCESS_TOKEN is set' do + before do + stub_env('PERSONAL_ACCESS_TOKEN', 'a_token') + end + + it 'returns specified token from env' do + expect(described_class.personal_access_token).to eq 'a_token' + end + end + + context 'when @personal_access_token is set' do + before do + described_class.personal_access_token = 'another_token' + end + + it 'returns the instance variable value' do + expect(described_class.personal_access_token).to eq 'another_token' + end + end + end + + describe '.personal_access_token=' do + around do |example| + described_class.instance_variable_set(:@personal_access_token, nil) + example.run + described_class.instance_variable_set(:@personal_access_token, nil) + end + + it 'saves the token' do + described_class.personal_access_token = 'a_token' + + expect(described_class.personal_access_token).to eq 'a_token' + end + end + describe '.forker?' do + before do + stub_env('GITLAB_FORKER_USERNAME', nil) + stub_env('GITLAB_FORKER_PASSWORD', nil) + end + it 'returns false if no forker credentials are defined' do expect(described_class).not_to be_forker end @@ -107,4 +181,30 @@ describe QA::Runtime::Env do expect { described_class.require_github_access_token! }.not_to raise_error end end + + describe '.log_destination' do + it 'returns $stdout if QA_LOG_PATH is not defined' do + stub_env('QA_LOG_PATH', nil) + + expect(described_class.log_destination).to eq($stdout) + end + + it 'returns the path if QA_LOG_PATH is defined' do + stub_env('QA_LOG_PATH', 'path/to_file') + + expect(described_class.log_destination).to eq('path/to_file') + end + end + + describe '.can_test?' do + it_behaves_like 'boolean method with parameter', + method: :can_test?, + param: :git_protocol_v2, + env_key: 'QA_CAN_TEST_GIT_PROTOCOL_V2', + default: true + + it 'raises ArgumentError if feature is unknown' do + expect { described_class.can_test? :foo }.to raise_error(ArgumentError, 'Unknown feature "foo"') + end + end end diff --git a/qa/spec/runtime/logger_spec.rb b/qa/spec/runtime/logger_spec.rb new file mode 100644 index 00000000000..44be3381bff --- /dev/null +++ b/qa/spec/runtime/logger_spec.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +describe QA::Runtime::Logger do + before do + logger = Logger.new $stdout + logger.level = ::Logger::DEBUG + described_class.logger = logger + end + + it 'logs debug' do + expect { described_class.debug('test') }.to output(/DEBUG -- : test/).to_stdout_from_any_process + end + + it 'logs info' do + expect { described_class.info('test') }.to output(/INFO -- : test/).to_stdout_from_any_process + end + + it 'logs warn' do + expect { described_class.warn('test') }.to output(/WARN -- : test/).to_stdout_from_any_process + end + + it 'logs error' do + expect { described_class.error('test') }.to output(/ERROR -- : test/).to_stdout_from_any_process + end + + it 'logs fatal' do + expect { described_class.fatal('test') }.to output(/FATAL -- : test/).to_stdout_from_any_process + end + + it 'logs unknown' do + expect { described_class.unknown('test') }.to output(/ANY -- : test/).to_stdout_from_any_process + end +end diff --git a/qa/spec/scenario/test/integration/instance_saml_spec.rb b/qa/spec/scenario/test/integration/instance_saml_spec.rb new file mode 100644 index 00000000000..cb8a6a630cc --- /dev/null +++ b/qa/spec/scenario/test/integration/instance_saml_spec.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +describe QA::Scenario::Test::Integration::InstanceSAML do + context '#perform' do + it_behaves_like 'a QA scenario class' do + let(:tags) { [:instance_saml] } + end + end +end diff --git a/qa/spec/scenario/test/integration/ldap_spec.rb b/qa/spec/scenario/test/integration/ldap_spec.rb index 198856aec3f..b6d798bf504 100644 --- a/qa/spec/scenario/test/integration/ldap_spec.rb +++ b/qa/spec/scenario/test/integration/ldap_spec.rb @@ -1,9 +1,17 @@ # frozen_string_literal: true -describe QA::Scenario::Test::Integration::LDAP do +describe QA::Scenario::Test::Integration::LDAPNoTLS do context '#perform' do it_behaves_like 'a QA scenario class' do - let(:tags) { [:ldap] } + let(:tags) { [:ldap_no_tls] } + end + end +end + +describe QA::Scenario::Test::Integration::LDAPTLS do + context '#perform' do + it_behaves_like 'a QA scenario class' do + let(:tags) { [:ldap_tls] } end end end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 8e6613cd688..8e01da01340 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -3,6 +3,10 @@ require_relative '../qa' Dir[::File.join(__dir__, 'support', '**', '*.rb')].each { |f| require f } RSpec.configure do |config| + config.before do |example| + QA::Runtime::Logger.debug("Starting test: #{example.full_description}") if QA::Runtime::Env.debug? + end + config.expect_with :rspec do |expectations| expectations.include_chain_clauses_in_custom_matcher_descriptions = true end diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index cf22d1c9395..741821ddf8c 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -62,6 +62,34 @@ describe QA::Specs::Runner do end end + context 'when SIGNUP_DISABLED is true' do + before do + allow(QA::Runtime::Env).to receive(:signup_disabled?).and_return(true) + end + + subject { described_class.new } + + it 'it includes default args and excludes the skip_signup_disabled tag' do + expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~skip_signup_disabled', *described_class::DEFAULT_TEST_PATH_ARGS]) + + subject.perform + end + end + + context 'when git protocol v2 is not supported' do + before do + allow(QA::Runtime::Env).to receive(:can_test?).with(:git_protocol_v2).and_return(false) + end + + subject { described_class.new } + + it 'it includes default args and excludes the requires_git_protocol_v2 tag' do + expect_rspec_runner_arguments(['--tag', '~orchestrated', '--tag', '~requires_git_protocol_v2', *described_class::DEFAULT_TEST_PATH_ARGS]) + + subject.perform + end + end + def expect_rspec_runner_arguments(arguments) expect(RSpec::Core::Runner).to receive(:run) .with(arguments, $stderr, $stdout) |