diff options
author | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
---|---|---|
committer | GitLab Bot <gitlab-bot@gitlab.com> | 2022-02-18 09:45:46 +0000 |
commit | a7b3560714b4d9cc4ab32dffcd1f74a284b93580 (patch) | |
tree | 7452bd5c3545c2fa67a28aa013835fb4fa071baf /qa | |
parent | ee9173579ae56a3dbfe5afe9f9410c65bb327ca7 (diff) | |
download | gitlab-ce-a7b3560714b4d9cc4ab32dffcd1f74a284b93580.tar.gz |
Add latest changes from gitlab-org/gitlab@14-8-stable-eev14.8.0-rc42
Diffstat (limited to 'qa')
158 files changed, 3192 insertions, 1412 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile index 54de6509518..fa666daa927 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -63,6 +63,7 @@ WORKDIR /home/gitlab/qa # Install qa dependencies or fetch from cache if unchanged COPY ./qa/Gemfile* /home/gitlab/qa/ +RUN gem install bundler --no-document --conservative --version 2.3.6 RUN bundle install --jobs=$(nproc) --retry=3 --without=development --quiet ## diff --git a/qa/Gemfile b/qa/Gemfile index c07f9dc96a7..1eaf9c7cec0 100644 --- a/qa/Gemfile +++ b/qa/Gemfile @@ -3,7 +3,7 @@ source 'https://rubygems.org' gem 'gitlab-qa', require: 'gitlab/qa' -gem 'activesupport', '~> 6.1.4.1' # This should stay in sync with the root's Gemfile +gem 'activesupport', '~> 6.1.4.6' # This should stay in sync with the root's Gemfile gem 'allure-rspec', '~> 2.15.0' gem 'capybara', '~> 3.35.0' gem 'capybara-screenshot', '~> 1.0.23' @@ -20,6 +20,7 @@ gem 'parallel_tests', '~> 2.29' gem 'rotp', '~> 3.1.0' gem 'timecop', '~> 0.9.1' gem 'parallel', '~> 1.19' +gem 'rainbow', '~> 3.0.0' gem 'rspec-parameterized', '~> 0.4.2' gem 'octokit', '~> 4.21' gem 'webdrivers', '~> 5.0' diff --git a/qa/Gemfile.lock b/qa/Gemfile.lock index 3e85a33f2a2..6afd205a6e1 100644 --- a/qa/Gemfile.lock +++ b/qa/Gemfile.lock @@ -2,7 +2,7 @@ GEM remote: https://rubygems.org/ specs: abstract_type (0.0.7) - activesupport (6.1.4.1) + activesupport (6.1.4.6) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -226,6 +226,7 @@ GEM rack (2.2.3) rack-test (1.1.0) rack (>= 1.0, < 3) + rainbow (3.0.0) rake (13.0.6) regexp_parser (2.1.1) representable (3.1.1) @@ -321,7 +322,7 @@ PLATFORMS ruby DEPENDENCIES - activesupport (~> 6.1.4.1) + activesupport (~> 6.1.4.6) airborne (~> 0.3.4) allure-rspec (~> 2.15.0) capybara (~> 3.35.0) @@ -339,6 +340,7 @@ DEPENDENCIES parallel (~> 1.19) parallel_tests (~> 2.29) pry-byebug (~> 3.5.1) + rainbow (~> 3.0.0) rake (~> 13) rest-client (~> 2.1.0) rotp (~> 3.1.0) @@ -355,4 +357,4 @@ DEPENDENCIES zeitwerk (~> 2.4) BUNDLED WITH - 2.2.33 + 2.3.6 diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb index cdd9bb20b42..b90a49abf4b 100644 --- a/qa/lib/gitlab/page/admin/subscription.rb +++ b/qa/lib/gitlab/page/admin/subscription.rb @@ -6,6 +6,10 @@ module Gitlab class Subscription < Chemlab::Page path '/admin/subscription' + div :subscription_details + text_field :activation_code + button :activate + label :terms_of_services, text: /I agree that/ p :plan p :started p :name @@ -16,6 +20,33 @@ module Gitlab h2 :users_in_subscription h2 :users_over_subscription table :subscription_history + + def accept_terms + terms_of_services_element.click # workaround for hidden checkbox + end + + # Checks if a subscription record exists in subscription history table + # + # @param plan [Hash] Name of the plan + # @option plan [Hash] Support::Helpers::FREE + # @option plan [Hash] Support::Helpers::PREMIUM + # @option plan [Hash] Support::Helpers::PREMIUM_SELF_MANAGED + # @option plan [Hash] Support::Helpers::ULTIMATE + # @option plan [Hash] Support::Helpers::ULTIMATE_SELF_MANAGED + # @option plan [Hash] Support::Helpers::CI_MINUTES + # @option plan [Hash] Support::Helpers::STORAGE + # @param users_in_license [Integer] Number of users in license + # @param license_type [Hash] Type of the license + # @option license_type [String] 'license file' + # @option license_type [String] 'cloud license' + # @return [Boolean] True if record exsists, false if not + def has_subscription_record?(plan, users_in_license, license_type) + # find any records that have a matching plan and seats and type + subscription_history_element.hashes.any? do |record| + record['Plan'] == plan[:name].capitalize && record['Seats'] == users_in_license.to_s && \ + record['Type'].strip.downcase == license_type + end + end end end end diff --git a/qa/lib/gitlab/page/admin/subscription.stub.rb b/qa/lib/gitlab/page/admin/subscription.stub.rb index 89d7bfb95d9..56a063e8978 100644 --- a/qa/lib/gitlab/page/admin/subscription.stub.rb +++ b/qa/lib/gitlab/page/admin/subscription.stub.rb @@ -4,6 +4,112 @@ module Gitlab module Page module Admin module Subscription + # @note Defined as +h6 :subscription_details+ + # @return [String] The text content or value of +subscription_details+ + def subscription_details + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription.subscription_details_element).to exist + # end + # @return [Watir::H6] The raw +H6+ element + def subscription_details_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription).to be_subscription_details + # end + # @return [Boolean] true if the +subscription_details+ element is present on the page + def subscription_details? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +text_field :activation_code+ + # @return [String] The text content or value of +activation_code+ + def activation_code + # This is a stub, used for indexing. The method is dynamically generated. + end + + # Set the value of activation_code + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # subscription.activation_code = 'value' + # end + # @param value [String] The value to set. + def activation_code=(value) + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription.activation_code_element).to exist + # end + # @return [Watir::TextField] The raw +TextField+ element + def activation_code_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription).to be_activation_code + # end + # @return [Boolean] true if the +activation_code+ element is present on the page + def activation_code? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +label :terms_of_services+ + # @return [String] The text content or value of +terms_of_services+ + def terms_of_services + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription.terms_of_services_element).to exist + # end + # @return [Watir::Label] The raw +Label+ element + def terms_of_services_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription).to be_terms_of_services + # end + # @return [Boolean] true if the +terms_of_services+ element is present on the page + def terms_of_services? + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @note Defined as +button :activate+ + # Clicks +activate+ + def activate + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription.activate_element).to exist + # end + # @return [Watir::Button] The raw +Button+ element + def activate_element + # This is a stub, used for indexing. The method is dynamically generated. + end + + # @example + # Gitlab::Page::Admin::Subscription.perform do |subscription| + # expect(subscription).to be_activate + # end + # @return [Boolean] true if the +activate+ element is present on the page + def activate? + # This is a stub, used for indexing. The method is dynamically generated. + end + # @note Defined as +p :plan+ # @return [String] The text content or value of +plan+ def plan @@ -51,7 +51,9 @@ module QA "smtp" => "SMTP", "otp" => "OTP", "jira_api" => "JiraAPI", - "registry_tls" => "RegistryTLS" + "registry_tls" => "RegistryTLS", + "jetbrains" => "JetBrains", + "vscode" => "VSCode" ) loader.setup diff --git a/qa/qa/fixtures/package_managers/composer/composer.json.erb b/qa/qa/fixtures/package_managers/composer/composer.json.erb new file mode 100644 index 00000000000..a1e31e2599f --- /dev/null +++ b/qa/qa/fixtures/package_managers/composer/composer.json.erb @@ -0,0 +1,13 @@ +{ + "name": "<%= project.path_with_namespace %>/<%= package.name %>", + "description": "Library XY", + "type": "library", + "license": "GPL-3.0-only", + "authors": [ + { + "name": "John Doe", + "email": "john@example.com" + } + ], + "require": {} +}
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/composer/composer_upload_package.yaml.erb b/qa/qa/fixtures/package_managers/composer/composer_upload_package.yaml.erb new file mode 100644 index 00000000000..b6bcfafffee --- /dev/null +++ b/qa/qa/fixtures/package_managers/composer/composer_upload_package.yaml.erb @@ -0,0 +1,13 @@ +publish: + image: curlimages/curl:latest + stage: build + variables: + URL: "$CI_SERVER_PROTOCOL://$CI_SERVER_HOST:$CI_SERVER_PORT/api/v4/projects/$CI_PROJECT_ID/packages/composer?job_token=$CI_JOB_TOKEN" + script: + - version=$([[ -z "$CI_COMMIT_TAG" ]] && echo "branch=$CI_COMMIT_REF_NAME" || echo "tag=$CI_COMMIT_TAG") + - insecure=$([ "$CI_SERVER_PROTOCOL" = "http" ] && echo "--insecure" || echo "") + - response=$(curl -s -w "%{http_code}" $insecure --data $version $URL) + - code=$(echo "$response" | tail -n 1) + - body=$(echo "$response" | head -n 1) + tags: + - "runner-for-<%= project.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/conan/conan_upload_install_package.yaml.erb b/qa/qa/fixtures/package_managers/conan/conan_upload_install_package.yaml.erb new file mode 100644 index 00000000000..39c04f6511b --- /dev/null +++ b/qa/qa/fixtures/package_managers/conan/conan_upload_install_package.yaml.erb @@ -0,0 +1,12 @@ +image: conanio/gcc7 + +test_package: + stage: deploy + script: + - conan remote add gitlab <%= gitlab_address_with_port %>/api/v4/projects/<%= project.id %>/packages/conan + - conan new <%= package.name %>/0.1 -t + - conan create . mycompany/stable + - "CONAN_LOGIN_USERNAME=ci_user CONAN_PASSWORD=${CI_JOB_TOKEN} conan upload <%= package.name %>/0.1@mycompany/stable --all --remote=gitlab" + - conan install <%= package.name %>/0.1@mycompany/stable --remote=gitlab + tags: + - runner-for-<%= project.name %>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/generic/generic_upload_install_package.yaml.erb b/qa/qa/fixtures/package_managers/generic/generic_upload_install_package.yaml.erb new file mode 100644 index 00000000000..13fe3e2c62e --- /dev/null +++ b/qa/qa/fixtures/package_managers/generic/generic_upload_install_package.yaml.erb @@ -0,0 +1,18 @@ +image: curlimages/curl:latest + +stages: + - upload + - download + +upload: + stage: upload + script: + - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file file.txt ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/<%= package.name %>/0.0.1/file.txt' + tags: + - runner-for-<%= project.name %> +download: + stage: download + script: + - 'wget --header="JOB-TOKEN: $CI_JOB_TOKEN" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/<%= package.name %>/0.0.1/file.txt -O file_downloaded.txt' + tags: + - runner-for-<%= project.name %>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/helm/Chart.yaml.erb b/qa/qa/fixtures/package_managers/helm/Chart.yaml.erb new file mode 100644 index 00000000000..5a56533c65d --- /dev/null +++ b/qa/qa/fixtures/package_managers/helm/Chart.yaml.erb @@ -0,0 +1,6 @@ +apiVersion: v2 +name: <%= package_name %> +description: GitLab QA helm package +type: application +version: <%= package_version %> +appVersion: "1.16.0"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/helm/helm_install_package.yaml.erb b/qa/qa/fixtures/package_managers/helm/helm_install_package.yaml.erb new file mode 100644 index 00000000000..786b0592153 --- /dev/null +++ b/qa/qa/fixtures/package_managers/helm/helm_install_package.yaml.erb @@ -0,0 +1,11 @@ +pull: + image: alpine:3 + script: + - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing + - helm repo add --username <%= username %> --password <%= access_token %> gitlab_qa ${CI_API_V4_URL}/projects/<%= package_project.id %>/packages/helm/stable + - helm repo update + - helm pull gitlab_qa/<%= package_name %> + only: + - <%= client_project.default_branch %> + tags: + - runner-for-<%=client_project.group.name %>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/helm/helm_upload_package.yaml.erb b/qa/qa/fixtures/package_managers/helm/helm_upload_package.yaml.erb new file mode 100644 index 00000000000..b3e907b50f4 --- /dev/null +++ b/qa/qa/fixtures/package_managers/helm/helm_upload_package.yaml.erb @@ -0,0 +1,14 @@ +deploy: + image: alpine:3 + script: + - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing + - apk add curl + - helm create <%= package_name %> + - cp ./Chart.yaml <%= package_name %> + - helm package <%= package_name %> + - http_code=$(curl --write-out "%{http_code}" --request POST --form 'chart=@<%= package_name %>-<%= package_version %>.tgz' --user <%= username %>:<%= access_token %> ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/api/stable/charts --output /dev/null --silent) + - '[ $http_code = "201" ]' + only: + - <%= package_project.default_branch %> + tags: + - runner-for-<%= package_project.group.name %>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/build_install.gradle.erb b/qa/qa/fixtures/package_managers/maven/build_install.gradle.erb new file mode 100644 index 00000000000..303a64ad233 --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/build_install.gradle.erb @@ -0,0 +1,28 @@ +plugins { + id 'java' + id 'application' +} + +repositories { + jcenter() + maven { + url "<%= gitlab_address_with_port %>/api/v4/projects/<%= package_project.id %>/packages/maven" + name "GitLab" + credentials(HttpHeaderCredentials) { + name = '<%= maven_header_name %>' + value = <%= token %> + } + authentication { + header(HttpHeaderAuthentication) + } + } +} + +dependencies { + implementation group: '<%= group_id %>', name: '<%= artifact_id %>', version: '<%= package_version %>' + testImplementation 'junit:junit:4.12' +} + +application { + mainClassName = 'gradle_maven_app.App' +}
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/build_upload.gradle.erb b/qa/qa/fixtures/package_managers/maven/build_upload.gradle.erb new file mode 100644 index 00000000000..c14e63e11df --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/build_upload.gradle.erb @@ -0,0 +1,27 @@ +plugins { + id 'java' + id 'maven-publish' +} + +publishing { + publications { + library(MavenPublication) { + groupId '<%= group_id %>' + artifactId '<%= artifact_id %>' + version '<%= package_version %>' + from components.java + } + } + repositories { + maven { + url "<%= gitlab_address_with_port %>/api/v4/projects/<%= package_project.id %>/packages/maven" + credentials(HttpHeaderCredentials) { + name = "Private-Token" + value = "<%= personal_access_token %>" + } + authentication { + header(HttpHeaderAuthentication) + } + } + } +}
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/client_pom.xml.erb b/qa/qa/fixtures/package_managers/maven/client_pom.xml.erb new file mode 100644 index 00000000000..20bb5f3964e --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/client_pom.xml.erb @@ -0,0 +1,19 @@ +<project> + <groupId><%= group_id %></groupId> + <artifactId>maven_client</artifactId> + <version>1.0</version> + <modelVersion>4.0.0</modelVersion> + <repositories> + <repository> + <id><%= package_project.name %></id> + <url><%= gitlab_address_with_port %>/api/v4/groups/<%= package_project.group.id %>/-/packages/maven</url> + </repository> + </repositories> + <dependencies> + <dependency> + <groupId><%= group_id %></groupId> + <artifactId><%= artifact_id %></artifactId> + <version><%= package_version %></version> + </dependency> + </dependencies> +</project>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/gradle_install_package.yaml.erb b/qa/qa/fixtures/package_managers/maven/gradle_install_package.yaml.erb new file mode 100644 index 00000000000..49873f124cc --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/gradle_install_package.yaml.erb @@ -0,0 +1,8 @@ + build: + image: gradle:6.5-jdk11 + script: + - 'gradle build' + only: + - "<%= client_project.default_branch %>" + tags: + - "runner-for-<%= client_project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/gradle_upload_package.yaml.erb b/qa/qa/fixtures/package_managers/maven/gradle_upload_package.yaml.erb new file mode 100644 index 00000000000..3f3c7dce03c --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/gradle_upload_package.yaml.erb @@ -0,0 +1,8 @@ +deploy: + image: gradle:6.5-jdk11 + script: + - 'gradle publish' + only: + - "<%= package_project.default_branch %>" + tags: + - "runner-for-<%= package_project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/maven_install_package.yaml.erb b/qa/qa/fixtures/package_managers/maven/maven_install_package.yaml.erb new file mode 100644 index 00000000000..78d6255e9a9 --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/maven_install_package.yaml.erb @@ -0,0 +1,8 @@ +install: + image: maven:3.6-jdk-11 + script: + - "mvn install -s settings.xml" + only: + - "<%= client_project.default_branch %>" + tags: + - "runner-for-<%= client_project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/maven_upload_package.yaml.erb b/qa/qa/fixtures/package_managers/maven/maven_upload_package.yaml.erb new file mode 100644 index 00000000000..64a63bf0bd8 --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/maven_upload_package.yaml.erb @@ -0,0 +1,8 @@ + deploy: + image: maven:3.6-jdk-11 + script: + - 'mvn deploy -s settings.xml' + only: + - "<%= package_project.default_branch %>" + tags: + - "runner-for-<%= package_project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/package_pom.xml.erb b/qa/qa/fixtures/package_managers/maven/package_pom.xml.erb new file mode 100644 index 00000000000..5159172a170 --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/package_pom.xml.erb @@ -0,0 +1,22 @@ + <project> + <groupId><%= group_id %></groupId> + <artifactId><%= artifact_id %></artifactId> + <version><%= package_version %></version> + <modelVersion>4.0.0</modelVersion> + <repositories> + <repository> + <id><%= package_project.name %></id> + <url><%= gitlab_address_with_port %>/api/v4/groups/<%= package_project.group.id %>/-/packages/maven</url> + </repository> + </repositories> + <distributionManagement> + <repository> + <id><%= package_project.name %></id> + <url><%= gitlab_address_with_port %>/api/v4/projects/<%= package_project.id %>/packages/maven</url> + </repository> + <snapshotRepository> + <id><%= package_project.name %></id> + <url><%= gitlab_address_with_port %>/api/v4/projects/<%= package_project.id %>/packages/maven</url> + </snapshotRepository> + </distributionManagement> +</project>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/settings.xml.erb b/qa/qa/fixtures/package_managers/maven/settings.xml.erb new file mode 100644 index 00000000000..b670b83cf85 --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/settings.xml.erb @@ -0,0 +1,16 @@ +<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" +xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> +<servers> + <server> + <id><%= package_project.name %></id> + <configuration> + <httpHeaders> + <property> + <name><%= maven_header_name %></name> + <value><%= token %></value> + </property> + </httpHeaders> + </configuration> + </server> +</servers> +</settings>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/maven/settings_with_pat.xml.erb b/qa/qa/fixtures/package_managers/maven/settings_with_pat.xml.erb new file mode 100644 index 00000000000..611c232819f --- /dev/null +++ b/qa/qa/fixtures/package_managers/maven/settings_with_pat.xml.erb @@ -0,0 +1,16 @@ +<settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" + xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> + <servers> + <server> + <id><%= package_project.name %></id> + <configuration> + <httpHeaders> + <property> + <name>Private-Token</name> + <value><%= personal_access_token %></value> + </property> + </httpHeaders> + </configuration> + </server> + </servers> +</settings>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/npm/npm_install_package_instance.yaml.erb b/qa/qa/fixtures/package_managers/npm/npm_install_package_instance.yaml.erb new file mode 100644 index 00000000000..a396fc98e95 --- /dev/null +++ b/qa/qa/fixtures/package_managers/npm/npm_install_package_instance.yaml.erb @@ -0,0 +1,21 @@ +image: node:latest + +stages: + - install + +install: + stage: install + script: + - "npm config set @<%= registry_scope %>:registry <%= gitlab_address_with_port %>/api/v4/packages/npm/" + - "npm install <%= package.name %>" + cache: + key: ${CI_BUILD_REF_NAME} + paths: + - node_modules/ + artifacts: + paths: + - node_modules/ + only: + - "<%= another_project.default_branch %>" + tags: + - "runner-for-<%= another_project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/npm/npm_upload_install_package_project.yaml.erb b/qa/qa/fixtures/package_managers/npm/npm_upload_install_package_project.yaml.erb new file mode 100644 index 00000000000..8d94d03ef9b --- /dev/null +++ b/qa/qa/fixtures/package_managers/npm/npm_upload_install_package_project.yaml.erb @@ -0,0 +1,31 @@ +image: node:latest + +stages: + - deploy + - install + +deploy: + stage: deploy + script: + - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=<%= auth_token %>">.npmrc + - npm publish + only: + - "<%= project.default_branch %>" + tags: + - "runner-for-<%= project.name %>" +install: + stage: install + script: + - "npm config set @<%= registry_scope %>:registry <%= gitlab_address_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" + - "npm install <%= package.name %>" + cache: + key: ${CI_BUILD_REF_NAME} + paths: + - node_modules/ + artifacts: + paths: + - node_modules/ + only: + - "<%= project.default_branch %>" + tags: + - "runner-for-<%= project.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/npm/npm_upload_package_instance.yaml.erb b/qa/qa/fixtures/package_managers/npm/npm_upload_package_instance.yaml.erb new file mode 100644 index 00000000000..13c00cd17c4 --- /dev/null +++ b/qa/qa/fixtures/package_managers/npm/npm_upload_package_instance.yaml.erb @@ -0,0 +1,14 @@ +image: node:latest + +stages: + - deploy + +deploy: + stage: deploy + script: + - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=<%= auth_token %>">.npmrc + - npm publish + only: + - "<%= project.default_branch %>" + tags: + - "runner-for-<%= project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/npm/package_instance.json.erb b/qa/qa/fixtures/package_managers/npm/package_instance.json.erb new file mode 100644 index 00000000000..46fecf97e2c --- /dev/null +++ b/qa/qa/fixtures/package_managers/npm/package_instance.json.erb @@ -0,0 +1,8 @@ +{ + "name": "<%= package.name %>", + "version": "1.0.0", + "description": "Example package for GitLab npm registry", + "publishConfig": { + "@<%= registry_scope %>:registry": "<%= gitlab_address_with_port %>/api/v4/projects/<%= project.id %>/packages/npm/" + } +}
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/npm/package_project.json.erb b/qa/qa/fixtures/package_managers/npm/package_project.json.erb new file mode 100644 index 00000000000..46fecf97e2c --- /dev/null +++ b/qa/qa/fixtures/package_managers/npm/package_project.json.erb @@ -0,0 +1,8 @@ +{ + "name": "<%= package.name %>", + "version": "1.0.0", + "description": "Example package for GitLab npm registry", + "publishConfig": { + "@<%= registry_scope %>:registry": "<%= gitlab_address_with_port %>/api/v4/projects/<%= project.id %>/packages/npm/" + } +}
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/nuget/nuget_install_package.yaml.erb b/qa/qa/fixtures/package_managers/nuget/nuget_install_package.yaml.erb new file mode 100644 index 00000000000..39b65a55884 --- /dev/null +++ b/qa/qa/fixtures/package_managers/nuget/nuget_install_package.yaml.erb @@ -0,0 +1,15 @@ +image: mcr.microsoft.com/dotnet/sdk:5.0 + +stages: + - install + +install: + stage: install + script: + - dotnet nuget locals all --clear + - dotnet nuget add source "$CI_SERVER_URL/api/v4/groups/<%= another_project.group.id %>/-/packages/nuget/index.json" --name gitlab --username <%= auth_token_username %> --password <%= auth_token_password %> --store-password-in-clear-text + - "dotnet add otherdotnet.csproj package <%= package.name %> --version 1.0.0" + only: + - "<%= another_project.default_branch %>" + tags: + - "runner-for-<%= project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/nuget/nuget_upload_package.yaml.erb b/qa/qa/fixtures/package_managers/nuget/nuget_upload_package.yaml.erb new file mode 100644 index 00000000000..7c88eb49be0 --- /dev/null +++ b/qa/qa/fixtures/package_managers/nuget/nuget_upload_package.yaml.erb @@ -0,0 +1,17 @@ +image: mcr.microsoft.com/dotnet/sdk:5.0 + +stages: + - deploy + +deploy: + stage: deploy + script: + - dotnet restore -p:Configuration=Release + - dotnet build -c Release + - dotnet pack -c Release -p:PackageID=<%= package.name %> + - dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username <%= auth_token_username %> --password <%= auth_token_password %> --store-password-in-clear-text + - dotnet nuget push "bin/Release/*.nupkg" --source gitlab + rules: + - if: '$CI_COMMIT_BRANCH == "<%= project.default_branch %>"' + tags: + - "runner-for-<%= project.group.name %>"
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/pypi/pypi_upload_install_package.yaml.erb b/qa/qa/fixtures/package_managers/pypi/pypi_upload_install_package.yaml.erb new file mode 100644 index 00000000000..3ea71152801 --- /dev/null +++ b/qa/qa/fixtures/package_managers/pypi/pypi_upload_install_package.yaml.erb @@ -0,0 +1,19 @@ +image: python:latest +stages: + - run + - install + +run: + stage: run + script: + - pip install twine + - python setup.py sdist bdist_wheel + - "TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url <%= gitlab_address_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*" + tags: + - runner-for-<%= project.name %> +install: + stage: install + script: + - "pip install <%= package.name %> --no-deps --index-url <%= uri.scheme %>://<%= personal_access_token %>:<%= personal_access_token %>@<%= gitlab_host_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host <%= gitlab_host_with_port %>" + tags: + - runner-for-<%= project.name %>
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/pypi/setup.py.erb b/qa/qa/fixtures/package_managers/pypi/setup.py.erb new file mode 100644 index 00000000000..d365f93cb5e --- /dev/null +++ b/qa/qa/fixtures/package_managers/pypi/setup.py.erb @@ -0,0 +1,16 @@ +import setuptools + +setuptools.setup( + name="<%= package.name %>", + version="0.0.1", + author="Example Author", + author_email="author@example.com", + description="A small example package", + packages=setuptools.find_packages(), + classifiers=[ + "Programming Language :: Python :: 3", + "License :: OSI Approved :: MIT License", + "Operating System :: OS Independent", + ], + python_requires='>=3.6', +)
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/rubygems/package.gemspec.erb b/qa/qa/fixtures/package_managers/rubygems/package.gemspec.erb new file mode 100644 index 00000000000..915deb0335d --- /dev/null +++ b/qa/qa/fixtures/package_managers/rubygems/package.gemspec.erb @@ -0,0 +1,39 @@ +# frozen_string_literal: true + +Gem::Specification.new do |s| + s.name = '<%= package.name %>' + s.authors = ['Tanuki Steve', 'Hal 9000'] + s.author = 'Tanuki Steve' + s.version = '0.0.1' + s.date = '2011-09-29' + s.summary = 'this is a test package' + s.files = ['lib/hello_gem.rb'] + s.require_paths = ['lib'] + + s.description = 'A test package for GitLab.' + s.email = 'tanuki@not_real.com' + s.homepage = 'https://gitlab.com/ruby-co/my-package' + s.license = 'MIT' + + s.metadata = { + 'bug_tracker_uri' => 'https://gitlab.com/ruby-co/my-package/issues', + 'changelog_uri' => 'https://gitlab.com/ruby-co/my-package/CHANGELOG.md', + 'documentation_uri' => 'https://gitlab.com/ruby-co/my-package/docs', + 'mailing_list_uri' => 'https://gitlab.com/ruby-co/my-package/mailme', + 'source_code_uri' => 'https://gitlab.com/ruby-co/my-package' + } + + s.bindir = 'bin' + s.platform = Gem::Platform::RUBY + s.post_install_message = 'Installed, thank you!' + s.rdoc_options = ['--main'] + s.required_ruby_version = '>= 2.7.0' + s.required_rubygems_version = '>= 1.8.11' + s.requirements = 'A high powered server or calculator' + s.rubygems_version = '1.8.09' + + s.add_dependency 'dependency_1', '~> 1.2.3' + s.add_dependency 'dependency_2', '3.0.0' + s.add_dependency 'dependency_3', '>= 1.0.0' + s.add_dependency 'dependency_4' +end
\ No newline at end of file diff --git a/qa/qa/fixtures/package_managers/rubygems/rubygems_upload_package.yaml.erb b/qa/qa/fixtures/package_managers/rubygems/rubygems_upload_package.yaml.erb new file mode 100644 index 00000000000..29038130f1b --- /dev/null +++ b/qa/qa/fixtures/package_managers/rubygems/rubygems_upload_package.yaml.erb @@ -0,0 +1,15 @@ +image: ruby + +test_package: + stage: deploy + before_script: + - mkdir ~/.gem + - echo "---" > ~/.gem/credentials + - | + echo "<%= gitlab_address_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/rubygems: '${CI_JOB_TOKEN}'" >> ~/.gem/credentials + - chmod 0600 ~/.gem/credentials + script: + - gem build <%= package.name %> + - gem push <%= package.name %>-0.0.1.gem --host <%= gitlab_address_with_port %>/api/v4/projects/${CI_PROJECT_ID}/packages/rubygems + tags: + - runner-for-<%= project.name %>
\ No newline at end of file diff --git a/qa/qa/page/component/badges.rb b/qa/qa/page/component/badges.rb new file mode 100644 index 00000000000..f2c5f809d8d --- /dev/null +++ b/qa/qa/page/component/badges.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + class Badges < Page::Base + view 'app/assets/javascripts/badges/components/badge_form.vue' do + element :badge_name_field + element :badge_link_url_field + element :badge_image_url_field + element :add_badge_button + end + + view 'app/assets/javascripts/badges/components/badge_list.vue' do + element :badge_list_content + element :badge_list_row + end + + view 'app/assets/javascripts/badges/components/badge.vue' do + element :badge_image_link + end + + def fill_name(name) + fill_element :badge_name_field, name + end + + def fill_link_url(url) + fill_element :badge_link_url_field, url + end + + def fill_image_url(url) + fill_element :badge_image_url_field, url + end + + def click_add_badge_button + click_element :add_badge_button + end + + def has_badge?(badge_name) + within_element(:badge_list_content) do + has_element?(:badge_list_row, badge_name: badge_name) + end + end + + def has_visible_badge_image_link?(link_url) + within_element(:badge_list_content) do + has_element?(:badge_image_link, link_url: link_url) + end + end + end + end + end +end diff --git a/qa/qa/page/component/blob_content.rb b/qa/qa/page/component/blob_content.rb index 4d36a6dcefe..ce743b24dda 100644 --- a/qa/qa/page/component/blob_content.rb +++ b/qa/qa/page/component/blob_content.rb @@ -22,6 +22,10 @@ module QA element :copy_contents_button end + base.view 'app/assets/javascripts/vue_shared/components/source_viewer/source_viewer.vue' do + element :blob_viewer_file_content + end + base.view 'app/views/projects/blob/_header_content.html.haml' do element :file_name_content end diff --git a/qa/qa/page/component/design_management.rb b/qa/qa/page/component/design_management.rb index 1f5620e30c7..73ba5713bda 100644 --- a/qa/qa/page/component/design_management.rb +++ b/qa/qa/page/component/design_management.rb @@ -55,7 +55,7 @@ module QA # wait for the "Save comment" button to disappear saved = has_no_element?(:save_comment_button) - raise ExpectationNotMet, %q(There was a problem while adding the annotation) unless saved + raise RSpec::Expectations::ExpectationNotMetError, %q(There was a problem while adding the annotation) unless saved end def add_design(design_file_path) diff --git a/qa/qa/page/component/invite_members_modal.rb b/qa/qa/page/component/invite_members_modal.rb index ca6862ccb02..7c536ff651b 100644 --- a/qa/qa/page/component/invite_members_modal.rb +++ b/qa/qa/page/component/invite_members_modal.rb @@ -9,7 +9,7 @@ module QA def self.included(base) super - base.view 'app/assets/javascripts/invite_members/components/invite_members_modal.vue' do + base.view 'app/assets/javascripts/invite_members/components/invite_modal_base.vue' do element :invite_button element :access_level_dropdown element :invite_members_modal_content @@ -44,9 +44,9 @@ module QA open_invite_members_modal within_element(:invite_members_modal_content) do - fill_element :members_token_select_input, username + fill_element(:members_token_select_input, username) Support::WaitForRequests.wait_for_requests - click_button username + click_button(username, match: :prefer_exact) set_access_level(access_level) end diff --git a/qa/qa/page/component/issuable/sidebar.rb b/qa/qa/page/component/issuable/sidebar.rb index 4a81230499c..921647eb4cc 100644 --- a/qa/qa/page/component/issuable/sidebar.rb +++ b/qa/qa/page/component/issuable/sidebar.rb @@ -40,7 +40,7 @@ module QA end base.view 'app/assets/javascripts/sidebar/components/sidebar_dropdown_widget.vue' do - element :milestone_link, 'data-qa-selector="`${issuableAttribute}_link`"' # rubocop:disable QA/ElementWithPattern + element :milestone_link, 'data-qa-selector="`${formatIssuableAttribute.snake}_link`"' # rubocop:disable QA/ElementWithPattern end base.view 'app/assets/javascripts/sidebar/components/sidebar_editable_item.vue' do diff --git a/qa/qa/page/component/members_filter.rb b/qa/qa/page/component/members_filter.rb new file mode 100644 index 00000000000..ac07fe7e9fa --- /dev/null +++ b/qa/qa/page/component/members_filter.rb @@ -0,0 +1,26 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module MembersFilter + extend QA::Page::PageConcern + + def self.included(base) + super + + base.view 'app/assets/javascripts/members/components/filter_sort/members_filtered_search_bar.vue' do + element :members_filtered_search_bar_content + end + end + + def search_member(username) + # TODO: Update the two actions below to use direct qa selectors once this is implemented: + # https://gitlab.com/gitlab-org/gitlab-ui/-/issues/1688 + find_element(:members_filtered_search_bar_content).find('input').set(username) + find('.gl-search-box-by-click-search-button').click + end + end + end + end +end diff --git a/qa/qa/page/component/namespace_select.rb b/qa/qa/page/component/namespace_select.rb new file mode 100644 index 00000000000..924e1af876c --- /dev/null +++ b/qa/qa/page/component/namespace_select.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module QA + module Page + module Component + module NamespaceSelect + extend QA::Page::PageConcern + + def self.included(base) + super + + base.view "app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue" do + element :namespaces_list + element :namespaces_list_groups + element :namespaces_list_item + end + end + + def select_namespace(item) + click_element :namespaces_list + + within_element(:namespaces_list) do + find_element(:namespaces_list_item, text: item).click + end + end + end + end + end +end diff --git a/qa/qa/page/component/wiki_page_form.rb b/qa/qa/page/component/wiki_page_form.rb index bc73fe0c3ab..8f504b784b2 100644 --- a/qa/qa/page/component/wiki_page_form.rb +++ b/qa/qa/page/component/wiki_page_form.rb @@ -38,7 +38,7 @@ module QA def click_submit click_element(:wiki_submit_button) - wait_until(reload: false) do + QA::Support::Retrier.retry_on_exception do has_no_element?(:wiki_title_textbox) end end @@ -48,15 +48,8 @@ module QA Page::Modal::DeleteWiki.perform(&:confirm_deletion) end - def use_new_editor(toggle) - # Update once the feature is released, see https://gitlab.com/gitlab-org/gitlab/-/issues/345398 - if toggle - click_element(:editing_mode_button, mode: 'Edit rich text') - else - within_element(:try_new_editor_container) do - click_button('Use the new editor') - end - end + def use_new_editor + click_element(:editing_mode_button, mode: 'Edit rich text') wait_until(reload: false) do has_element?(:content_editor_container) diff --git a/qa/qa/page/group/members.rb b/qa/qa/page/group/members.rb index ccc901932f4..c80bdadb11f 100644 --- a/qa/qa/page/group/members.rb +++ b/qa/qa/page/group/members.rb @@ -6,6 +6,7 @@ module QA class Members < Page::Base include Page::Component::InviteMembersModal include Page::Component::UsersSelect + include Page::Component::MembersFilter view 'app/assets/javascripts/members/components/modals/remove_member_modal.vue' do element :remove_member_modal_content @@ -31,6 +32,8 @@ module QA end def update_access_level(username, access_level) + search_member(username) + within_element(:member_row, text: username) do click_element :access_level_dropdown click_element :access_level_link, text: access_level diff --git a/qa/qa/page/group/settings/general.rb b/qa/qa/page/group/settings/general.rb index 2e7ab131225..1877065f478 100644 --- a/qa/qa/page/group/settings/general.rb +++ b/qa/qa/page/group/settings/general.rb @@ -7,6 +7,8 @@ module QA class General < QA::Page::Base include ::QA::Page::Settings::Common include Page::Component::VisibilitySetting + include Page::Component::ConfirmModal + include Page::Component::NamespaceSelect view 'app/views/groups/edit.html.haml' do element :permission_lfs_2fa_content @@ -38,16 +40,6 @@ module QA element :project_creation_level_dropdown end - view 'app/views/groups/settings/_transfer.html.haml' do - element :select_group_dropdown - element :transfer_group_button - end - - view 'app/helpers/dropdowns_helper.rb' do - element :dropdown_input_field - element :dropdown_list_content - end - def set_group_name(name) find_element(:group_name_field).send_keys([:command, 'a'], :backspace) find_element(:group_name_field).set name @@ -111,17 +103,14 @@ module QA click_element(:save_permissions_changes_button) end - def transfer_group(target_group) + def transfer_group(target_group, source_group) expand_content :advanced_settings_content - click_element :select_group_dropdown - fill_element(:dropdown_input_field, target_group) - - within_element(:dropdown_list_content) do - click_on target_group - end + select_namespace(target_group) + click_element(:transfer_button) - click_element :transfer_group_button + fill_confirmation_text(source_group) + confirm_transfer end end end diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb index f004107d7bd..a5bd37be287 100644 --- a/qa/qa/page/main/login.rb +++ b/qa/qa/page/main/login.rb @@ -157,6 +157,7 @@ module QA end def redirect_to_login_page(address) + Menu.perform(&:sign_out_if_signed_in) desired_host = URI(Runtime::Scenario.send("#{address}_address")).host Runtime::Browser.visit(address, Page::Main::Login) if desired_host != current_host end diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index f8d063ac6bd..d76dfb295a0 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -83,10 +83,18 @@ module QA element :merge_immediately_menu_item end + view 'app/assets/javascripts/vue_merge_request_widget/components/states/sha_mismatch.vue' do + element :head_mismatch_content + end + view 'app/assets/javascripts/vue_merge_request_widget/components/states/squash_before_merge.vue' do element :squash_checkbox end + view 'app/assets/javascripts/vue_merge_request_widget/mr_widget_options.vue' do + element :mr_widget_content + end + view 'app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue' do element :apply_suggestion_dropdown element :commit_message_field @@ -255,7 +263,8 @@ module QA # status as unmerged, the test will fail. # Revisit after merge page re-architect is done https://gitlab.com/groups/gitlab-org/-/epics/5598 # To remove page refresh logic if possible - retry_until(max_attempts: 3, reload: true) do + # We don't raise on failure because this method is used as a predicate matcher + retry_until(max_attempts: 3, reload: true, raise_on_failure: false) do has_element?(:merged_status_content, text: 'The changes were merged into', wait: 20) end end @@ -269,13 +278,29 @@ module QA has_element?(:merge_button, disabled: false) end - # Waits up 60 seconds and raises an error if unable to merge - def wait_until_ready_to_merge - has_element?(:merge_button) + # Waits up 60 seconds and raises an error if unable to merge. + # + # If a state is encountered in which a user would typically refresh the page, this will refresh the page and + # then check again if it's ready to merge. For example, it will refresh if a new change was pushed and the page + # needs to be refreshed to show the change. + # + # @param [Boolean] transient_test true if the current test is a transient test (default: false) + def wait_until_ready_to_merge(transient_test: false) + wait_until do + has_element?(:merge_button) - # The merge button is enabled via JS - wait_until(reload: false) do - !find_element(:merge_button).disabled? + break true unless find_element(:merge_button).disabled? + + # If the widget shows "Merge blocked: new changes were just added" we can refresh the page and check again + next false if has_element?(:head_mismatch_content) + + # Stop waiting if we're in a transient test. By this point we're in an unexpected state and should let the + # test fail so we can investigate. If we're not in a transient test we keep trying until we reach timeout. + next true unless transient_test + + QA::Runtime::Logger.debug("MR widget text: #{mr_widget_text}") + + false end end @@ -385,6 +410,10 @@ module QA def cancel_auto_merge! click_element(:cancel_auto_merge_button) end + + def mr_widget_text + find_element(:mr_widget_content).text + end end end end diff --git a/qa/qa/page/project/members.rb b/qa/qa/page/project/members.rb index 1102abd6646..30748ed920b 100644 --- a/qa/qa/page/project/members.rb +++ b/qa/qa/page/project/members.rb @@ -5,6 +5,7 @@ module QA module Project class Members < Page::Base include QA::Page::Component::InviteMembersModal + include QA::Page::Component::MembersFilter view 'app/assets/javascripts/members/components/members_tabs.vue' do element :groups_list_tab diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 42baf1f3f87..e061bc52abc 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -13,7 +13,6 @@ module QA view 'app/views/projects/_new_project_fields.html.haml' do element :initialize_with_readme_checkbox - element :initialize_with_sast_checkbox 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 @@ -21,6 +20,10 @@ module QA element :visibility_radios, 'visibility_level:' # rubocop:disable QA/ElementWithPattern end + view 'app/views/projects/_new_project_initialize_with_sast.html.haml' do + element :initialize_with_sast_checkbox + end + view 'app/views/projects/project_templates/_template.html.haml' do element :use_template_button element :template_option_row diff --git a/qa/qa/page/project/pipeline/index.rb b/qa/qa/page/project/pipeline/index.rb index f7c5d149593..d088ba76bc0 100644 --- a/qa/qa/page/project/pipeline/index.rb +++ b/qa/qa/page/project/pipeline/index.rb @@ -26,11 +26,15 @@ module QA end def wait_for_latest_pipeline_succeeded - wait_for_latest_pipeline_status { has_text?('passed') } + wait_for_latest_pipeline_status { has_selector?(".ci-status-icon-success") } end def wait_for_latest_pipeline_completed - wait_for_latest_pipeline_status { has_text?('passed') || has_text?('failed') } + wait_for_latest_pipeline_status { has_selector?(".ci-status-icon-success") || has_selector?(".ci-status-icon-failed") } + end + + def wait_for_latest_pipeline_skipped + wait_for_latest_pipeline_status { has_text?('skipped') } end def wait_for_latest_pipeline_status diff --git a/qa/qa/page/project/pipeline/show.rb b/qa/qa/page/project/pipeline/show.rb index 83a49ae6361..6f4757a34e8 100644 --- a/qa/qa/page/project/pipeline/show.rb +++ b/qa/qa/page/project/pipeline/show.rb @@ -18,7 +18,7 @@ module QA view 'app/assets/javascripts/pipelines/components/graph/job_item.vue' do element :job_item_container element :job_link - element :action_button + element :job_action_button end view 'app/assets/javascripts/pipelines/components/graph/linked_pipeline.vue' do @@ -38,6 +38,11 @@ module QA element :pipeline_badges end + view 'app/assets/javascripts/pipelines/components/graph/job_group_dropdown.vue' do + element :job_dropdown_container + element :jobs_dropdown_menu + end + def running?(wait: 0) within_element(:pipeline_header) do page.has_content?('running', wait: wait) @@ -47,7 +52,7 @@ module QA def has_build?(name, status: :success, wait: nil) if status within_element(:job_item_container, text: name) do - has_selector?(".ci-status-icon-#{status}", { wait: wait }.compact) + has_selector?(".ci-status-icon-#{status}", **{ wait: wait }.compact) end else has_element?(:job_item_container, text: name) @@ -110,8 +115,22 @@ module QA end def click_job_action(job_name) + wait_for_requests + within_element(:job_item_container, text: job_name) do - click_element(:action_button) + click_element(:job_action_button) + end + end + + def click_job_dropdown(job_dropdown_name) + click_element(:job_dropdown_container, text: job_dropdown_name) + end + + def has_skipped_job_in_group? + within_element(:jobs_dropdown_menu) do + all_elements(:job_item_container, minimum: 1).all? do + has_selector?('.ci-status-icon-skipped') + end end end end diff --git a/qa/qa/page/project/settings/advanced.rb b/qa/qa/page/project/settings/advanced.rb index da1f16f4cfc..525210a08f6 100644 --- a/qa/qa/page/project/settings/advanced.rb +++ b/qa/qa/page/project/settings/advanced.rb @@ -6,18 +6,13 @@ module QA module Settings class Advanced < Page::Base include Component::ConfirmModal + include Component::NamespaceSelect view 'app/views/projects/edit.html.haml' do element :project_path_field element :change_path_button end - view "app/assets/javascripts/vue_shared/components/namespace_select/namespace_select.vue" do - element :namespaces_list - element :namespaces_list_groups - element :namespaces_list_item - end - view 'app/views/projects/settings/_archive.html.haml' do element :archive_project_link element :unarchive_project_link @@ -43,14 +38,6 @@ module QA click_element :change_path_button end - def select_namespace(item) - click_element :namespaces_list - - within_element(:namespaces_list) do - find_element(:namespaces_list_item, text: item).click - end - end - def transfer_project!(project_name, namespace) QA::Runtime::Logger.info "Transferring project: #{project_name} to namespace: #{namespace}" diff --git a/qa/qa/page/project/settings/main.rb b/qa/qa/page/project/settings/main.rb index 5efcb7bf23c..52ed630ac66 100644 --- a/qa/qa/page/project/settings/main.rb +++ b/qa/qa/page/project/settings/main.rb @@ -9,11 +9,13 @@ module QA include Component::Select2 include SubMenus::Project include Component::Breadcrumbs + include Layout::Flash view 'app/views/projects/edit.html.haml' do element :advanced_settings_content element :merge_request_settings_content element :visibility_features_permissions_content + element :badges_settings_content end view 'app/views/projects/settings/_general.html.haml' do @@ -51,6 +53,12 @@ module QA VisibilityFeaturesPermissions.perform(&block) end end + + def expand_badges_settings(&block) + expand_content(:badges_settings_content) do + Component::Badges.perform(&block) + end + end end end end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 8074b6f833b..4c9df2716e2 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -39,6 +39,8 @@ module QA element :forked_from_link element :project_name_content element :project_id_content + element :project_badges_content + element :badge_image_link end view 'app/views/projects/_files.html.haml' do @@ -179,6 +181,12 @@ module QA has_css?('.tree-holder') end end + + def has_visible_badge_image_link?(link_url) + within_element(:project_badges_content) do + has_element?(:badge_image_link, link_url: link_url) + end + end end end end diff --git a/qa/qa/page/project/web_ide/edit.rb b/qa/qa/page/project/web_ide/edit.rb index 9c0a3ab691c..403c919c6e5 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -68,7 +68,7 @@ module QA element :delete_button end - view 'app/views/shared/_confirm_fork_modal.html.haml' do + view 'app/assets/javascripts/vue_shared/components/confirm_fork_modal.vue' do element :fork_project_button element :confirm_fork_modal end diff --git a/qa/qa/page/trials/new.rb b/qa/qa/page/trials/new.rb index 6e9d7fce688..cd3b145a89e 100644 --- a/qa/qa/page/trials/new.rb +++ b/qa/qa/page/trials/new.rb @@ -12,6 +12,7 @@ module QA select :number_of_employees text_field :telephone_number select :country + select :state, id: 'state' button :continue end end diff --git a/qa/qa/resource/api_fabricator.rb b/qa/qa/resource/api_fabricator.rb index 4c77c515cfd..1958884916c 100644 --- a/qa/qa/resource/api_fabricator.rb +++ b/qa/qa/resource/api_fabricator.rb @@ -7,11 +7,11 @@ module QA module Resource module ApiFabricator include Capybara::DSL + include Support::API include Errors - attr_reader :api_resource, :api_response attr_writer :api_client - attr_accessor :api_user + attr_accessor :api_user, :api_resource, :api_response def api_support? respond_to?(:api_get_path) && @@ -48,9 +48,6 @@ module QA end end - include Support::API - attr_writer :api_resource, :api_response - def api_put(body = api_put_body) response = put( Runtime::API::Request.new(api_client, api_put_path).url, @@ -67,6 +64,16 @@ module QA @api_fabrication_http_method ||= :post end + # Checks if a resource already exists + # + # @return [Boolean] true if the resource returns HTTP status code 200 + def exists? + request = Runtime::API::Request.new(api_client, api_get_path) + response = get(request.url) + + response.code == HTTP_STATUS_OK + end + private def resource_web_url(resource) diff --git a/qa/qa/resource/badge_base.rb b/qa/qa/resource/badge_base.rb new file mode 100644 index 00000000000..5bb7eb98d4e --- /dev/null +++ b/qa/qa/resource/badge_base.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +module QA + module Resource + class BadgeBase < Base + attributes :id, + :name, + :link_url, + :image_url + + def initialize + @name = "qa-badge-#{SecureRandom.hex(8)}" + end + + def fabricate! + Page::Component::Badges.perform do |badges| + badges.fill_name(name) + badges.fill_link_url(link_url) + badges.fill_image_url(image_url) + badges.click_add_badge_button + end + end + end + end +end diff --git a/qa/qa/resource/base.rb b/qa/qa/resource/base.rb index 0112e766cf0..fc7f8445d4e 100644 --- a/qa/qa/resource/base.rb +++ b/qa/qa/resource/base.rb @@ -8,6 +8,7 @@ module QA class Base include ApiFabricator extend Capybara::DSL + using Rainbow NoValueError = Class.new(RuntimeError) @@ -31,7 +32,7 @@ module QA 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) } + log_and_record_fabrication(:browser_ui, resource, parents, args) { resource.fabricate!(*args) } current_url end @@ -47,7 +48,7 @@ module QA 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! } + log_and_record_fabrication(:api, resource, parents, args) { resource.fabricate_via_api! } end end @@ -59,7 +60,7 @@ module QA resource.eager_load_api_client! do_fabricate!(resource: resource, prepare_block: prepare_block, parents: parents) do - log_fabrication(:api, resource, parents, args) { resource.remove_via_api! } + log_and_record_fabrication(:api, resource, parents, args) { resource.remove_via_api! } end end @@ -71,36 +72,17 @@ module QA resource_web_url = yield resource.web_url = resource_web_url - QA::Tools::TestResourceDataProcessor.collect(resource, resource_identifier(resource)) - resource end - def resource_identifier(resource) - if resource.respond_to?(:username) && resource.username - "with username '#{resource.username}'" - elsif resource.respond_to?(:full_path) && resource.full_path - "with full_path '#{resource.full_path}'" - elsif resource.respond_to?(:name) && resource.name - "with name '#{resource.name}'" - elsif resource.respond_to?(:id) && resource.id - "with id '#{resource.id}'" - elsif resource.respond_to?(:iid) && resource.iid - "with iid '#{resource.iid}'" - end - rescue QA::Resource::Base::NoValueError - nil - end - - def log_fabrication(method, resource, parents, args) + def log_and_record_fabrication(fabrication_method, resource, parents, args) start = Time.now Support::FabricationTracker.start_fabrication result = yield.tap do fabrication_time = Time.now - start - fabrication_http_method = if resource.api_fabrication_http_method == :get - if self.include?(Reusable) + if include?(Reusable) "Retrieved for reuse" else "Retrieved" @@ -109,16 +91,23 @@ module QA "Built" end - Support::FabricationTracker.save_fabrication(:"#{method}_fabrication", fabrication_time) + Support::FabricationTracker.save_fabrication(:"#{fabrication_method}_fabrication", fabrication_time) + Tools::TestResourceDataProcessor.collect( + resource: resource, + info: resource.identifier, + fabrication_method: fabrication_method, + fabrication_time: fabrication_time + ) + Runtime::Logger.debug do msg = ["==#{'=' * parents.size}>"] - msg << "#{fabrication_http_method} a #{name}" - msg << resource_identifier(resource) if resource_identifier(resource) + msg << "#{fabrication_http_method} a #{Rainbow(name).black.bg(:white)}" + msg << resource.identifier msg << "as a dependency of #{parents.last}" if parents.any? - msg << "via #{method}" + msg << "via #{fabrication_method}" msg << "in #{fabrication_time} seconds" - msg.join(' ') + msg.compact.join(' ') end end Support::FabricationTracker.finish_fabrication @@ -172,7 +161,7 @@ module QA end def visit!(skip_resp_code_check: false) - Runtime::Logger.debug(%(Visiting #{self.class.name} at "#{web_url}")) + Runtime::Logger.debug("Visiting #{Rainbow(self.class.name).black.bg(:white)} at #{web_url}") # Just in case an async action is not yet complete Support::WaitForRequests.wait_for_requests(skip_resp_code_check: skip_resp_code_check) @@ -209,6 +198,35 @@ module QA JSON.pretty_generate(comparable) end + def diff(other) + return if self == other + + diff_values = self.comparable.to_a - other.comparable.to_a + diff_values.to_h + end + + def identifier + if respond_to?(:username) && username + "with username '#{username}'" + elsif respond_to?(:full_path) && full_path + "with full_path '#{full_path}'" + elsif respond_to?(:name) && name + "with name '#{name}'" + elsif respond_to?(:id) && id + "with id '#{id}'" + elsif respond_to?(:iid) && iid + "with iid '#{iid}'" + end + rescue QA::Resource::Base::NoValueError + nil + end + + def remove_via_api! + super + + Runtime::Logger.debug(["Removed a #{self.class.name}", identifier].compact.join(' ')) + end + protected # Custom resource comparison logic using resource attributes from api_resource diff --git a/qa/qa/resource/clusters/agent.rb b/qa/qa/resource/clusters/agent.rb index ee5a292b9b3..b190634f357 100644 --- a/qa/qa/resource/clusters/agent.rb +++ b/qa/qa/resource/clusters/agent.rb @@ -17,7 +17,6 @@ module QA end def fabricate! - puts 'TODO: FABRICATE VIA UI' end def resource_web_url(resource) diff --git a/qa/qa/resource/clusters/agent_token.rb b/qa/qa/resource/clusters/agent_token.rb index 6d803b94564..c1cf5c2f37b 100644 --- a/qa/qa/resource/clusters/agent_token.rb +++ b/qa/qa/resource/clusters/agent_token.rb @@ -11,7 +11,6 @@ module QA end def fabricate! - puts 'TODO: FABRICATE VIA UI' end def resource_web_url(resource) diff --git a/qa/qa/resource/fork.rb b/qa/qa/resource/fork.rb index d0313670e8b..d60b90b534f 100644 --- a/qa/qa/resource/fork.rb +++ b/qa/qa/resource/fork.rb @@ -31,6 +31,8 @@ module QA end end + delegate :path_with_namespace, to: :project + def fabricate! populate(:upstream, :user) diff --git a/qa/qa/resource/group.rb b/qa/qa/resource/group.rb index a325d96ccc2..dee63f9699c 100644 --- a/qa/qa/resource/group.rb +++ b/qa/qa/resource/group.rb @@ -3,12 +3,16 @@ module QA module Resource class Group < GroupBase - attributes :require_two_factor_authentication, :description + attributes :require_two_factor_authentication, :description, :path attribute :full_path do determine_full_path end + attribute :name do + @name || path + end + attribute :sandbox do Sandbox.fabricate_via_api! do |sandbox| sandbox.api_client = api_client @@ -50,12 +54,6 @@ module QA resource_web_url(api_get) rescue ResourceNotFoundError super - - Support::Retrier.retry_on_exception(sleep_interval: 5) do - resource = resource_web_url(api_get) - populate(:runners_token) - resource - end end def api_get_path @@ -66,7 +64,7 @@ module QA { parent_id: sandbox.id, path: path, - name: path, + name: name || path, visibility: 'public', require_two_factor_authentication: @require_two_factor_authentication, avatar: avatar diff --git a/qa/qa/resource/group_badge.rb b/qa/qa/resource/group_badge.rb index 3719b502b93..0c176bd5fbc 100644 --- a/qa/qa/resource/group_badge.rb +++ b/qa/qa/resource/group_badge.rb @@ -2,12 +2,8 @@ module QA module Resource - class GroupBadge < Base - attributes :id, - :name, - :link_url, - :image_url, - :group + class GroupBadge < BadgeBase + attribute :group # API get path # diff --git a/qa/qa/resource/group_base.rb b/qa/qa/resource/group_base.rb index 9f492a046db..05b41a4b4f6 100644 --- a/qa/qa/resource/group_base.rb +++ b/qa/qa/resource/group_base.rb @@ -7,6 +7,8 @@ module QA class GroupBase < Base include Members + MAX_NAME_LENGTH = 255 + attr_accessor :path, :avatar attributes :id, diff --git a/qa/qa/resource/group_milestone.rb b/qa/qa/resource/group_milestone.rb index b9ec53e929c..c208270658e 100644 --- a/qa/qa/resource/group_milestone.rb +++ b/qa/qa/resource/group_milestone.rb @@ -14,7 +14,7 @@ module QA attribute :group do Group.fabricate_via_api! do |resource| - resource.name = 'group-with-milestone' + resource.name = "group-with-milestone-#{SecureRandom.hex(4)}" end end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 0750ea49224..c5b72eebe03 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -17,6 +17,7 @@ module QA attributes :id, :name, + :path, :add_name_uuid, :description, :runners_token, diff --git a/qa/qa/resource/project_badge.rb b/qa/qa/resource/project_badge.rb new file mode 100644 index 00000000000..e036999117e --- /dev/null +++ b/qa/qa/resource/project_badge.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +module QA + module Resource + class ProjectBadge < BadgeBase + def initialize + super + + @link_url = "#{Runtime::Scenario.gitlab_address}/%{project_path}" + @image_url = "#{Runtime::Scenario.gitlab_address}/%{project_path}/badges/%{default_branch}/pipeline.svg" + end + + def fabricate! + Page::Project::Menu.perform(&:go_to_general_settings) + Page::Project::Settings::Main.perform(&:expand_badges_settings) + + super + end + end + end +end diff --git a/qa/qa/resource/project_web_hook.rb b/qa/qa/resource/project_web_hook.rb new file mode 100644 index 00000000000..8b806c42030 --- /dev/null +++ b/qa/qa/resource/project_web_hook.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module QA + module Resource + class ProjectWebHook < Base + EVENT_TRIGGERS = %i[ + issues + job + merge_requests + note + pipeline + push + tag_push + wiki_page + confidential_issues + confidential_note + ].freeze + + attr_accessor :url, :enable_ssl, :id + + attribute :project do + Project.fabricate_via_api! do |resource| + resource.name = 'project-with-webhooks' + end + end + + EVENT_TRIGGERS.each do |trigger| + attribute "#{trigger}_events".to_sym do + false + end + end + + def initialize + @id = nil + @enable_ssl = false + @url = nil + end + + def resource_web_url(resource) + "/project/#{project.name}/~/hooks/##{resource[:id]}/edit" + end + + def api_get_path + "/projects/#{project.id}/hooks" + end + + def api_post_path + api_get_path + end + + def api_post_body + body = { + id: project.id, + url: url, + enable_ssl_verification: enable_ssl + } + EVENT_TRIGGERS.each_with_object(body) do |trigger, memo| + attr = "#{trigger}_events" + memo[attr.to_sym] = send(attr) + memo + end + end + end + end +end diff --git a/qa/qa/resource/protected_branch.rb b/qa/qa/resource/protected_branch.rb index 7db6450acf8..062d4e9f3d8 100644 --- a/qa/qa/resource/protected_branch.rb +++ b/qa/qa/resource/protected_branch.rb @@ -61,6 +61,8 @@ module QA end def fabricate_via_api! + resource_web_url(api_get) + rescue ResourceNotFoundError populate_new_branch_if_required super @@ -75,7 +77,11 @@ module QA end def api_delete_path - "/projects/#{project.id}/protected_branches/#{branch_name}" + api_get_path + end + + def api_put_path + api_get_path end def api_post_path @@ -107,6 +113,16 @@ module QA # this particular resource does not expose a web_url property end + def set_require_code_owner_approval(require = true) + response = patch(Runtime::API::Request.new(api_client, api_put_path).url, { code_owner_approval_required: require }) + return if response.code == HTTP_STATUS_OK + + raise( + ResourceUpdateFailedError, + "Could not update code_owner_approval_required to #{require}. Request returned (#{response.code}): `#{response}`." + ) + end + class Roles NO_ONE = { description: 'No one', access_level: 0 }.freeze DEVS_AND_MAINTAINERS = { description: 'Developers + Maintainers', access_level: 30 }.freeze diff --git a/qa/qa/resource/reusable.rb b/qa/qa/resource/reusable.rb index 24b0a1f6bce..6a9d0392ba2 100644 --- a/qa/qa/resource/reusable.rb +++ b/qa/qa/resource/reusable.rb @@ -4,8 +4,13 @@ module QA module Resource # # This module includes methods that allow resource classes to be reused safely. It should be prepended to a new - # reusable version of an existing resource class. See Resource::Project and ReusableResource::Project for an example + # reusable version of an existing resource class. See Resource::Project and ReusableResource::Project for an example. + # Reusable resource classes must also be registered with a resource collection that will manage cleanup. # + # @example Register a resource class with a collection + # QA::Resource::ReusableCollection.register_resource_classes do |collection| + # QA::Resource::ReusableProject.register(collection) + # end module Reusable attr_accessor :reuse, :reuse_as @@ -16,7 +21,7 @@ module QA base.extend(ClassMethods) end - # Gets an existing resource if it exists and the parameters of the new specification of the resource are valid. + # Gets an existing resource if it exists and the specified attributes of the resource are valid. # Creates a new instance of the resource if it does not exist. # # @return [String] The URL of the resource. @@ -27,33 +32,128 @@ module QA rescue Errors::ResourceNotFoundError super ensure - self.class.resources[reuse_as] = self + self.class.resources[reuse_as] ||= { + tests: Set.new, + resource: self + } + + self.class.resources[reuse_as][:attributes] ||= all_attributes.each_with_object({}) do |attribute_name, attributes| + attributes[attribute_name] = instance_variable_get("@#{attribute_name}") + end + self.class.resources[reuse_as][:tests] << Runtime::Example.location end - # Including classes must confirm that the resource can be reused as defined. For example, a project can't be - # fabricated with a unique name. + # Overrides remove_via_api! to log a debug message stating that removal will happen after the suite completes. # # @return [nil] + def remove_via_api! + QA::Runtime::Logger.debug("#{self.class.name} - deferring removal until after suite") + end + + # Object comparison + # + # @param [QA::Resource::Base] other + # @return [Boolean] + def ==(other) + self.class <= other.class && comparable == other.comparable + end + + # Confirms that reuse of the resource did not change it in a way that breaks later reuse. + # For example, this should fail if a reusable resource should have a specific name, but the name has been changed. + def validate_reuse + QA::Runtime::Logger.debug(["Validating a #{self.class.name} that was reused as #{reuse_as}", identifier].compact.join(' ')) + + fresh_resource = reference_resource + diff = reuse_validation_diff(fresh_resource) + + if diff.present? + raise ResourceReuseError, <<~ERROR + The reused #{self.class.name} resource does not have the attributes expected. + The following change was found: #{diff}" + The resource's web_url is #{web_url}. + It was used in these tests: #{self.class.resources[reuse_as][:tests].to_a.join(', ')} + ERROR + end + + ensure + fresh_resource.remove_via_api! + end + + private + + # Creates a new resource that can be compared to a reused resource, using the post body of the original. + # Must be implemented by classes that include this module. + def reference_resource + return super if defined?(super) + + raise NotImplementedError + end + + # Confirms that the resource attributes specified in its fabricate_via_api! block will allow it to be reused. + # + # @return [nil] returns nil unless an error is raised def validate_reuse_preconditions + return unless self.class.resources.key?(reuse_as) + + attributes = unique_identifiers.each_with_object({ proposed: {}, existing: {} }) do |id, attrs| + proposed = public_send(id) + existing = self.class.resources[reuse_as][:resource].public_send(id) + + next if proposed == existing + + attrs[:proposed][id] = proposed + attrs[:existing][id] = existing + end + + unless attributes[:proposed].empty? && attributes[:existing].empty? + raise ResourceReuseError, "Reusable resources must use the same unique identifier(s). " \ + "The #{self.class.name} to be reused as :#{reuse_as} has the identifier(s) #{attributes[:proposed]} " \ + "but it should have #{attributes[:existing]}" + end + end + + # Compares the attributes of the current reused resource with a reference instance. + # + # @return [Hash] any differences between the resources. + def reuse_validation_diff(other) + original, reference = prepare_reuse_validation_diff(other) + + return if original == reference + + diff_values = original.to_a - reference.to_a + diff_values.to_h + end + + # Compares the current reusable resource to a reference instance, ignoring identifying unique attributes that + # had to be changed. + # + # @return [Hash, Hash] the current and reference resource attributes, respectively. + def prepare_reuse_validation_diff(other) + original = self.reload!.comparable + reference = other.reload!.comparable + unique_identifiers.each { |id| reference[id] = original[id] } + [original, reference] + end + + # The attributes of the resource that should be the same whenever a test wants to reuse a resource. Must be + # implemented by classes that include this module. + # + # @return [Array<Symbol>] the attribute names. + def unique_identifiers return super if defined?(super) raise NotImplementedError end module ClassMethods - # Removes all created resources of this type. - # - # @return [Hash<Symbol, QA::Resource>] the resources that were to be removed. - def remove_all_via_api! - resources.each do |reuse_as, resource| - QA::Runtime::Logger.debug("#{self.name} - removing #{reuse_as}") - resource.method(:remove_via_api!).super_method.call - end + # Includes the resources created/reused by this class in the specified collection + def register(collection) + collection[self.name] = resources end - # The resources created by this resource class. + # The resources created/reused by this resource class. # - # @return [Hash<Symbol, QA::Resource>] the resources created by this resource class. + # @return [Hash<Symbol, Hash>] the resources created/reused by this resource class. def resources @resources ||= {} end diff --git a/qa/qa/resource/reusable_collection.rb b/qa/qa/resource/reusable_collection.rb new file mode 100644 index 00000000000..1168b0091fc --- /dev/null +++ b/qa/qa/resource/reusable_collection.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +require 'singleton' + +module QA + module Resource + # + # This singleton class collects all reusable resources used by tests and allows operations to be performed on them + # all. For example, verifying their state after tests have run and might have changed them. + # + class ReusableCollection + include Singleton + + attr_accessor :resource_classes + + def initialize + @resource_classes = {} + end + + # Yields each resource in the collection. + # + # @yieldparam [Symbol] reuse_as the name that identifies the resource instance. + # @yieldparam [QA::Resource] reuse_instance the resource. + def each_resource + resource_classes.each_value do |reuse_instances| + reuse_instances.each do |reuse_as, reuse_instance| + yield reuse_as, reuse_instance[:resource] + end + end + end + + class << self + # Removes all created resources that are included in the collection. + def remove_all_via_api! + instance.each_resource do |reuse_as, resource| + next QA::Runtime::Logger.debug("#{resource.class.name} reused as :#{reuse_as} has already been removed.") unless resource.exists? + + resource.method(:remove_via_api!).super_method.call + end + end + + # Validates the reuse of each resource as defined by the resource class of each resource in the collection. + def validate_resource_reuse + instance.each_resource { |_, resource| resource.validate_reuse } + end + + # Yields the collection of resources to allow resource classes to register themselves with the collection. + # + # @yieldparam [Hash] resource_classes the resource classes in the collection. + def register_resource_classes + yield instance.resource_classes + end + end + end + end +end diff --git a/qa/qa/resource/reusable_group.rb b/qa/qa/resource/reusable_group.rb index a4bd799e85c..b75cb0517bf 100644 --- a/qa/qa/resource/reusable_group.rb +++ b/qa/qa/resource/reusable_group.rb @@ -8,46 +8,35 @@ module QA def initialize super - @path = "reusable_group" + @name = @path = 'reusable_group' @description = "QA reusable group" @reuse_as = :default_group end - # Confirms that the group can be reused - # - # @return [nil] returns nil unless an error is raised - def validate_reuse_preconditions - unless reused_path_unique? - raise ResourceReuseError, - "Reusable groups must have the same name. The group reused as #{reuse_as} has the path '#{path}' but it should be '#{self.class.resources[reuse_as].path}'" - end - end + private - # Confirms that reuse of the resource did not change it in a way that breaks later reuse. This raises an error if - # the current group path doesn't match the original path. - def validate_reuse - reload! - - if api_resource[:path] != @path - raise ResourceReuseError, "The group now has the path '#{api_resource[:path]}' but it should be '#{path}'" - end - end - - # Checks if the group is being reused with the same path. + # Creates a new group that can be compared to a reused group, using the attributes of the original. Attributes that + # must be unique (path and name) are replaced with new unique values. # - # @return [Boolean] true if the group's path is different from another group with the same reuse symbol (reuse_as) - def reused_path_unique? - return true unless self.class.resources.key?(reuse_as) - - self.class.resources[reuse_as].path == path + # @return [QA::Resource] a new instance of Resource::ReusableGroup that should be a copy of the original resource + def reference_resource + attributes = self.class.resources[reuse_as][:attributes] + name = "ref#{SecureRandom.hex(8)}_#{attributes.delete(:path)}"[0...MAX_NAME_LENGTH] + + Group.fabricate_via_api! do |resource| + self.class.resources[reuse_as][:attributes].each do |attribute_name, attribute_value| + resource.instance_variable_set("@#{attribute_name}", attribute_value) if attribute_value + end + resource.path = name + resource.name = name + end end - # Overrides QA::Resource::Group#remove_via_api! to log a debug message stating that removal will happen after - # the suite completes rather than now. + # The attributes of the resource that should be the same whenever a test wants to reuse a group. # - # @return [nil] - def remove_via_api! - QA::Runtime::Logger.debug("#{self.class.name} - deferring removal until after suite") + # @return [Array<Symbol>] the attribute names. + def unique_identifiers + [:name, :path] end end end diff --git a/qa/qa/resource/reusable_project.rb b/qa/qa/resource/reusable_project.rb index d2dfff8ad56..b9fca314122 100644 --- a/qa/qa/resource/reusable_project.rb +++ b/qa/qa/resource/reusable_project.rb @@ -15,36 +15,36 @@ module QA super @add_name_uuid = false - @name = "reusable_project" + @name = @path = 'reusable_project' @reuse_as = :default_project @initialize_with_readme = true end - # Confirms that the project can be reused - # - # @return [nil] returns nil unless an error is raised - def validate_reuse_preconditions - unless reused_name_unique? - raise ResourceReuseError, - "Reusable projects must have the same name. The project reused as #{reuse_as} has the name '#{name}' but it should be '#{self.class.resources[reuse_as].name}'" - end - end + private - # Checks if the project is being reused with the same name. + # Creates a new project that can be compared to a reused project, using the attributes of the original. Attributes + # that must be unique (path and name) are replaced with new unique values. # - # @return [Boolean] true if the project's name is different from another project with the same reuse symbol (reuse_as) - def reused_name_unique? - return true unless self.class.resources.key?(reuse_as) - - self.class.resources[reuse_as].name == name + # @return [QA::Resource] a new instance of Resource::ReusableProject that should be a copy of the original resource + def reference_resource + attributes = self.class.resources[reuse_as][:attributes] + name = "reference_resource_#{SecureRandom.hex(8)}_for_#{attributes.delete(:name)}" + + Project.fabricate_via_api! do |project| + self.class.resources[reuse_as][:attributes].each do |attribute_name, attribute_value| + project.instance_variable_set("@#{attribute_name}", attribute_value) if attribute_value + end + project.name = name + project.path = name + project.path_with_namespace = "#{project.group.full_path}/#{project.name}" + end end - # Overrides QA::Resource::Project#remove_via_api! to log a debug message stating that removal will happen after - # the suite completes rather than now. + # The attributes of the resource that should be the same whenever a test wants to reuse a project. # - # @return [nil] - def remove_via_api! - QA::Runtime::Logger.debug("#{self.class.name} - deferring removal until after suite") + # @return [Array<Symbol>] the attribute names. + def unique_identifiers + [:name, :path] end end end diff --git a/qa/qa/resource/sandbox.rb b/qa/qa/resource/sandbox.rb index 555bfb1abc9..8e7527bccd4 100644 --- a/qa/qa/resource/sandbox.rb +++ b/qa/qa/resource/sandbox.rb @@ -51,14 +51,6 @@ module QA resource_web_url(api_get) rescue ResourceNotFoundError super - - # If the group was just created the runners token might not be - # available via the API immediately. - Support::Retrier.retry_on_exception(sleep_interval: 5) do - resource = resource_web_url(api_get) - populate(:runners_token) - resource - end end def api_get_path diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 1679698a9c0..088822cc2ca 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -65,6 +65,10 @@ module QA ENV['QA_LOG_PATH'] || $stdout end + def colorized_logs? + enabled?(ENV['COLORIZED_LOGS'], default: false) + end + # set to 'false' to have the browser run visibly instead of headless def webdriver_headless? if ENV.key?('CHROME_HEADLESS') @@ -291,6 +295,14 @@ module QA ENV['JIRA_HOSTNAME'] end + # this is set by the integrations job + # which will allow bidirectional communication + # between the app and the specs container + # should the specs container spin up a server + def qa_hostname + ENV['QA_HOSTNAME'] + end + def cache_namespace_name? enabled?(ENV['CACHE_NAMESPACE_NAME'], default: true) end @@ -434,6 +446,18 @@ module QA ENV.fetch('QA_TEST_RESOURCES_CREATED_FILEPATH', File.join(Path.qa_root, 'tmp', file_name)) end + def ee_activation_code + ENV['QA_EE_ACTIVATION_CODE'] + end + + def quarantine_disabled? + enabled?(ENV['DISABLE_QUARANTINE'], default: false) + end + + def validate_resource_reuse? + enabled?(ENV['QA_VALIDATE_RESOURCE_REUSE'], default: false) + end + private def remote_grid_credentials diff --git a/qa/qa/runtime/example.rb b/qa/qa/runtime/example.rb new file mode 100644 index 00000000000..cd2119762f5 --- /dev/null +++ b/qa/qa/runtime/example.rb @@ -0,0 +1,15 @@ +# frozen_string_literal: true + +module QA + module Runtime + module Example + extend self + + attr_accessor :current + + def location + current.respond_to?(:location) ? current.location : 'unknown' + end + end + end +end diff --git a/qa/qa/runtime/fixtures.rb b/qa/qa/runtime/fixtures.rb index 05dee4bfce5..41d7ce5d178 100644 --- a/qa/qa/runtime/fixtures.rb +++ b/qa/qa/runtime/fixtures.rb @@ -33,6 +33,14 @@ module QA FileUtils.remove_entry(dir, true) end + def read_fixture(fixture_path, file_name) + file_path = Pathname + .new(__dir__) + .join("../fixtures/#{fixture_path}/#{file_name}") + + File.read(file_path) + end + private def api_client diff --git a/qa/qa/runtime/logger.rb b/qa/qa/runtime/logger.rb index a70c8faf7d2..81c41000033 100644 --- a/qa/qa/runtime/logger.rb +++ b/qa/qa/runtime/logger.rb @@ -2,11 +2,13 @@ require 'logger' require 'forwardable' +require 'rainbow/refinement' module QA module Runtime module Logger extend SingleForwardable + using Rainbow def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unknown @@ -14,8 +16,16 @@ module QA attr_writer :logger def logger + Rainbow.enabled = Runtime::Env.colorized_logs? + @logger ||= ::Logger.new(Runtime::Env.log_destination).tap do |logger| logger.level = Runtime::Env.debug? ? ::Logger::DEBUG : ::Logger::ERROR + + logger.formatter = proc do |severity, datetime, progname, msg| + date_format = datetime.strftime("%Y-%m-%d %H:%M:%S") + + "[date=#{date_format} from=QA Tests] #{severity.ljust(5)} -- ".yellow + "#{msg}\n" + end end end end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index ef634d3ccda..8cf1fa0705f 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -32,6 +32,9 @@ module QA # Given *gitlab_address* = 'http://gitlab-abc123.test/' #=> http://about.gitlab-abc123.test/ Runtime::Scenario.define(:about_address, URI(-> { gitlab_address.host = "about.#{gitlab_address.host}"; gitlab_address }.call).to_s) # rubocop:disable Style/Semicolon + # Save the scenario class name + Runtime::Scenario.define(:klass, self.class.name) + ## # Setup knapsack and download latest report # diff --git a/qa/qa/service/cluster_provider/gcloud.rb b/qa/qa/service/cluster_provider/gcloud.rb index c6d1f6cfe88..77677745f7a 100644 --- a/qa/qa/service/cluster_provider/gcloud.rb +++ b/qa/qa/service/cluster_provider/gcloud.rb @@ -49,7 +49,7 @@ module QA if account.empty? raise "Failed to login to gcloud. No credentials provided in environment and no credentials found locally." else - puts "gcloud account found. Using: #{account} for creating K8s cluster." + QA::Runtime::Logger.debug("gcloud account found. Using: #{account} for creating K8s cluster.") end end end diff --git a/qa/qa/service/docker_run/smocker.rb b/qa/qa/service/docker_run/smocker.rb new file mode 100644 index 00000000000..83ab58887da --- /dev/null +++ b/qa/qa/service/docker_run/smocker.rb @@ -0,0 +1,56 @@ +# frozen_string_literal: true + +module QA + module Service + module DockerRun + class Smocker < Base + def initialize + @image = 'thiht/smocker:0.17.1' + @name = 'smocker-server' + @public_port = '8080' + @admin_port = '8081' + super + @network_cache = network + end + + def host_name + return '127.0.0.1' unless QA::Runtime::Env.running_in_ci? || QA::Runtime::Env.qa_hostname + + "#{@name}.#{@network_cache}" + end + + def base_url + "http://#{host_name}:#{@public_port}" + end + + def admin_url + "http://#{host_name}:#{@admin_port}" + end + + def wait_for_running + Support::Waiter.wait_until(raise_on_failure: false, reload_page: false) do + running? + end + end + + def register! + command = <<~CMD.tr("\n", ' ') + docker run -d --rm + --network #{@network_cache} + --hostname #{host_name} + --name #{@name} + --publish #{@public_port}:8080 + --publish #{@admin_port}:8081 + #{@image} + CMD + + unless QA::Runtime::Env.running_in_ci? || QA::Runtime::Env.qa_hostname + command.gsub!("--network #{@network_cache} ", '') + end + + shell command + end + end + end + end +end diff --git a/qa/qa/service/praefect_manager.rb b/qa/qa/service/praefect_manager.rb index 7e47049d446..8ffb7c47652 100644 --- a/qa/qa/service/praefect_manager.rb +++ b/qa/qa/service/praefect_manager.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'digest' + module QA module Service class PraefectManager @@ -50,6 +52,7 @@ module QA def stop_primary_node stop_node(@primary_node) + wait_until_node_is_removed_from_healthy_storages(@primary_node) end def start_primary_node @@ -67,6 +70,7 @@ module QA def stop_secondary_node stop_node(@secondary_node) + wait_until_node_is_removed_from_healthy_storages(@secondary_node) end def start_secondary_node @@ -75,6 +79,7 @@ module QA def stop_tertiary_node stop_node(@tertiary_node) + wait_until_node_is_removed_from_healthy_storages(@tertiary_node) end def start_tertiary_node @@ -82,20 +87,41 @@ module QA end def start_node(name) - shell "docker start #{name}" - end + state = node_state(name) + return if state == "running" + + if state == "paused" + shell "docker unpause #{name}" + end + + if state == "stopped" + shell "docker start #{name}" + end - def stop_node(name) - shell "docker stop #{name}" wait_until_shell_command_matches( "docker inspect -f {{.State.Running}} #{name}", - /false/, + /true/, sleep_interval: 3, max_duration: 180, retry_on_exception: true ) end + def stop_node(name) + return if node_state(name) == 'paused' + + shell "docker pause #{name}" + end + + def node_state(name) + state = "stopped" + wait_until_shell_command("docker inspect -f {{.State.Status}} #{name}") do |line| + QA::Runtime::Logger.debug(line) + break state = "running" if line.include?("running") + break state = "paused" if line.include?("paused") + end + end + def clear_replication_queue QA::Runtime::Logger.info("Clearing the replication queue") shell sql_to_docker_exec_cmd( @@ -174,15 +200,25 @@ module QA end def start_all_nodes - start_node(@postgres) + start_postgres start_node(@primary_node) start_node(@secondary_node) start_node(@tertiary_node) - start_node(@praefect) + start_praefect wait_for_health_check_all_nodes end + def start_postgres + start_node(@postgres) + + Support::Waiter.repeat_until(max_attempts: 60, sleep_interval: 1) do + shell(sql_to_docker_exec_cmd("SELECT 1 as healthy_database"), fail_on_exception: false) do |line| + break true if line.include?("healthy_database") + end + end + end + def verify_storage_move(source_storage, destination_storage, repo_type: :project) return if Specs::Helpers::ContextSelector.dot_com? @@ -194,9 +230,8 @@ module QA def wait_for_praefect QA::Runtime::Logger.info("Waiting for health check on praefect") Support::Waiter.wait_until(max_duration: 120, sleep_interval: 1, raise_on_failure: true) do - # praefect runs a grpc server on port 2305, which will return an error 'Connection refused' until such time it is ready - wait_until_shell_command("docker exec #{@gitaly_cluster} bash -c 'curl #{@praefect}:2305'") do |line| - break if line.include?('curl: (1) Received HTTP/0.9 when not allowed') + wait_until_shell_command("docker exec #{@praefect} gitlab-ctl status praefect") do |line| + break true if line.include?('run: praefect: ') QA::Runtime::Logger.debug(line.chomp) end @@ -250,6 +285,48 @@ module QA end end + def praefect_dataloss_information(project_id) + dataloss_info = [] + cmd = "docker exec #{@praefect} praefect -config /var/opt/gitlab/praefect/config.toml dataloss --partially-unavailable=true" + shell(cmd) { |line| dataloss_info << line.strip } + + # Expected will have a record for each repository in the storage, in the following format + # @hashed/bc/52/bc52dd634277c4a34a2d6210994a9a5e2ab6d33bb4a3a8963410e00ca6c15a02.git: + # Primary: gitaly1 + # In-Sync Storages: + # gitaly1, assigned host + # gitaly3, assigned host + # Outdated Storages: + # gitaly2 is behind by 1 change or less, assigned host + # + # Alternatively, if all repositories are in sync, a concise message is returned + # Virtual storage: default + # All repositories are fully available on all assigned storages! + + # extract the relevant project under test info if it is identified + start_index = dataloss_info.index { |line| line.include?("#{Digest::SHA256.hexdigest(project_id.to_s)}.git") } + unless start_index.nil? + dataloss_info = dataloss_info[start_index, 7] + end + + dataloss_info&.each { |info| QA::Runtime::Logger.debug(info) } + dataloss_info + end + + def praefect_dataloss_info_for_project(project_id) + dataloss_info = [] + Support::Retrier.retry_until(max_duration: 60) do + dataloss_info = praefect_dataloss_information(project_id) + dataloss_info.include?("#{Digest::SHA256.hexdigest(project_id.to_s)}.git") + end + end + + def wait_for_project_synced_across_all_storages(project_id) + Support::Retrier.retry_until(max_duration: 60) do + praefect_dataloss_information(project_id).include?('All repositories are fully available on all assigned storages!') + end + end + def wait_for_health_check_all_nodes wait_for_gitaly_health_check(@primary_node) wait_for_gitaly_health_check(@secondary_node) @@ -259,9 +336,8 @@ module QA def wait_for_gitaly_health_check(node) QA::Runtime::Logger.info("Waiting for health check on #{node}") Support::Waiter.wait_until(max_duration: 120, sleep_interval: 1, raise_on_failure: true) do - # gitaly runs a grpc server on port 8075, which will return an error 'Connection refused' until such time it is ready - wait_until_shell_command("docker exec #{@praefect} bash -c 'curl #{node}:8075'") do |line| - break if line.include?('curl: (1) Received HTTP/0.9 when not allowed') + wait_until_shell_command("docker exec #{node} gitlab-ctl status gitaly") do |line| + break true if line.include?('run: gitaly: ') QA::Runtime::Logger.debug(line.chomp) end diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb index 5a35d8c251e..33d1d10b515 100644 --- a/qa/qa/service/shellout.rb +++ b/qa/qa/service/shellout.rb @@ -5,6 +5,7 @@ require 'open3' module QA module Service module Shellout + using Rainbow CommandError = Class.new(StandardError) module_function @@ -13,23 +14,25 @@ module QA # TODO, make it possible to use generic QA framework classes # as a library - gitlab-org/gitlab-qa#94 # - def shell(command, stdin_data: nil) - puts "Executing `#{command}`" + def shell(command, stdin_data: nil, fail_on_exception: true) + QA::Runtime::Logger.info("Executing `#{command}`".cyan) Open3.popen2e(*command) do |stdin, out, wait| stdin.puts(stdin_data) if stdin_data stdin.close if stdin_data + cmd_output = '' if block_given? out.each do |line| + cmd_output += line yield line end end out.each_char { |char| print char } - if wait.value.exited? && wait.value.exitstatus.nonzero? - raise CommandError, "Command `#{command}` failed!" + if wait.value.exited? && wait.value.exitstatus.nonzero? && fail_on_exception + raise CommandError, "Command failed: #{command} \nCommand Output: #{cmd_output}" end end end diff --git a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb index 4bc95395f25..a51d733d484 100644 --- a/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_github_repo_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :github, :requires_admin do + RSpec.describe 'Manage', :github, :requires_admin, :reliable do describe 'Project import' do let!(:api_client) { Runtime::API::Client.as_admin } let!(:group) { Resource::Group.fabricate_via_api! { |resource| resource.api_client = api_client } } diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb index 8a2a382ac45..bb4b0472398 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_issue_spec.rb @@ -3,12 +3,8 @@ require_relative 'gitlab_project_migration_common' module QA - RSpec.describe 'Manage', :requires_admin do - describe 'Gitlab migration', quarantine: { - only: { job: 'praefect' }, - type: :investigating, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348999' - } do + RSpec.describe 'Manage' do + describe 'Gitlab migration' do include_context 'with gitlab project migration' context 'with project issues' do @@ -40,13 +36,13 @@ module QA testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347608' ) do expect_import_finished + expect(imported_issues.count).to eq(1) aggregate_failures do - expect(imported_issues.count).to eq(1) expect(imported_issue).to eq(source_issue.reload!) expect(imported_comments.count).to eq(1) - expect(imported_comments.first[:body]).to include(source_comment[:body]) + expect(imported_comments.first&.fetch(:body)).to include(source_comment[:body]) end end end diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb index 9dce9bff3c1..d656ea4dea5 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_mr_spec.rb @@ -3,12 +3,8 @@ require_relative 'gitlab_project_migration_common' module QA - RSpec.describe 'Manage', :requires_admin do - describe 'Gitlab migration', quarantine: { - only: { job: 'praefect' }, - type: :investigating, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348999' - } do + RSpec.describe 'Manage' do + describe 'Gitlab migration' do include_context 'with gitlab project migration' context 'with merge request' do @@ -33,7 +29,8 @@ module QA let!(:source_comment) { source_mr.add_comment('This is a test comment!') } let(:imported_mrs) { imported_project.merge_requests } - let(:imported_mr_comments) { imported_mr.comments } + let(:imported_mr_comments) { imported_mr.comments.map { |note| note.except(:id, :noteable_id) } } + let(:source_mr_comments) { source_mr.comments.map { |note| note.except(:id, :noteable_id) } } let(:imported_mr) do Resource::MergeRequest.init do |mr| @@ -52,17 +49,12 @@ module QA testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348478' ) do expect_import_finished + expect(imported_mrs.count).to eq(1) aggregate_failures do - expect(imported_mrs.count).to eq(1) - # TODO: remove custom comparison after member migration is implemented - # https://gitlab.com/gitlab-org/gitlab/-/issues/341886 - expect(imported_mr.comparable.except(:author)).to eq(source_mr.reload!.comparable.except(:author)) + expect(imported_mr).to eq(source_mr.reload!) - expect(imported_mr_comments.count).to eq(1) - expect(imported_mr_comments.first[:body]).to include(source_comment[:body]) - # Comment will have mention of original user since members are not migrated yet - expect(imported_mr_comments.first[:body]).to include(other_user.name) + expect(imported_mr_comments).to eq(source_mr_comments) end end end diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb index a0c758c99e6..421dbe56a99 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_project_spec.rb @@ -3,12 +3,8 @@ require_relative 'gitlab_project_migration_common' module QA - RSpec.describe 'Manage', :requires_admin do - describe 'Gitlab migration', quarantine: { - only: { job: 'praefect' }, - type: :investigating, - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348999' - } do + RSpec.describe 'Manage' do + describe 'Gitlab migration' do include_context 'with gitlab project migration' context 'with uninitialized project' do diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb index 827ebc1f5e2..b7f0a10c525 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_project_migration_common.rb @@ -1,7 +1,13 @@ # frozen_string_literal: true module QA - RSpec.shared_context 'with gitlab project migration' do + # Disable on staging until bulk_import_projects toggle is on by default + # Otherwise tests running in parallel can disable feature in the middle of other test + RSpec.shared_context 'with gitlab project migration', :requires_admin, except: { subdomain: :staging }, quarantine: { + only: { job: 'praefect' }, + type: :investigating, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/348999' + } do let(:source_project_with_readme) { false } let(:import_wait_duration) { { max_duration: 300, sleep_interval: 2 } } let(:admin_api_client) { Runtime::API::Client.as_admin } diff --git a/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb b/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb index 13a795ca976..6480b880400 100644 --- a/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb +++ b/qa/qa/specs/features/api/1_manage/project_access_token_spec.rb @@ -78,11 +78,6 @@ module QA @different_project.remove_via_api! end end - - after(:all) do - @project_access_token.remove_via_api! - @project_access_token.project.remove_via_api! - end end end end diff --git a/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb b/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb index 6a31d173440..fe6c89f4ee4 100644 --- a/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb +++ b/qa/qa/specs/features/api/1_manage/user_access_termination_spec.rb @@ -31,44 +31,50 @@ module QA end it 'is not allowed to push code via the CLI', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347863' do - expect do - Resource::Repository::Push.fabricate! do |push| - push.repository_http_uri = @project.repository_http_location.uri - push.file_name = 'test.txt' - push.file_content = "# This is a test project named #{@project.name}" - push.commit_message = 'Add test.txt' - push.branch_name = "new_branch_#{SecureRandom.hex(8)}" - push.user = @user - end - end.to raise_error(QA::Support::Run::CommandError, /You are not allowed to push code to this project/) + QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 2) do + expect do + Resource::Repository::Push.fabricate! do |push| + push.repository_http_uri = @project.repository_http_location.uri + push.file_name = 'test.txt' + push.file_content = "# This is a test project named #{@project.name}" + push.commit_message = 'Add test.txt' + push.branch_name = "new_branch_#{SecureRandom.hex(8)}" + push.user = @user + end + end.to raise_error(QA::Support::Run::CommandError, /You are not allowed to push code to this project/) + end end it 'is not allowed to create a file via the API', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347864' do - expect do - Resource::File.fabricate_via_api! do |file| - file.api_client = @user_api_client - file.project = @project - file.branch = "new_branch_#{SecureRandom.hex(8)}" - file.commit_message = 'Add new file' - file.name = 'test.txt' - file.content = "New file" - end - end.to raise_error(Resource::ApiFabricator::ResourceFabricationFailedError, /403 Forbidden/) + QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 2) do + expect do + Resource::File.fabricate_via_api! do |file| + file.api_client = @user_api_client + file.project = @project + file.branch = "new_branch_#{SecureRandom.hex(8)}" + file.commit_message = 'Add new file' + file.name = 'test.txt' + file.content = "New file" + end + end.to raise_error(Resource::ApiFabricator::ResourceFabricationFailedError, /403 Forbidden/) + end end it 'is not allowed to commit via the API', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347865' do - expect do - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.api_client = @user_api_client - commit.project = @project - commit.branch = "new_branch_#{SecureRandom.hex(8)}" - commit.start_branch = @project.default_branch - commit.commit_message = 'Add new file' - commit.add_files([ - { file_path: 'test.txt', content: 'new file' } - ]) - end - end.to raise_error(Resource::ApiFabricator::ResourceFabricationFailedError, /403 Forbidden - You are not allowed to push into this branch/) + QA::Support::Retrier.retry_on_exception(max_attempts: 5, sleep_interval: 2) do + expect do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.api_client = @user_api_client + commit.project = @project + commit.branch = "new_branch_#{SecureRandom.hex(8)}" + commit.start_branch = @project.default_branch + commit.commit_message = 'Add new file' + commit.add_files([ + { file_path: 'test.txt', content: 'new file' } + ]) + end + end.to raise_error(Resource::ApiFabricator::ResourceFabricationFailedError, /403 Forbidden - You are not allowed to push into this branch/) + end end end diff --git a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb index 6a9be19efdd..55ae0d215cf 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/automatic_failover_and_recovery_spec.rb @@ -9,37 +9,30 @@ module QA project = nil let(:intial_commit_message) { 'Initial commit' } - let(:first_added_commit_message) { 'pushed to primary gitaly node' } - let(:second_added_commit_message) { 'commit to failover node' } + let(:first_added_commit_message) { 'first_added_commit_message to primary gitaly node' } + let(:second_added_commit_message) { 'second_added_commit_message to failover node' } before(:context) do - # Reset the cluster in case previous tests left it in a bad state praefect_manager.start_all_nodes project = Resource::Project.fabricate! do |project| project.name = "gitaly_cluster" project.initialize_with_readme = true end - end - - after do - praefect_manager.start_all_nodes + # We need to ensure that the the project is replicated to all nodes before proceeding with this test + praefect_manager.wait_for_replication(project.id) end it 'automatically fails over', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347830' do - # Create a new project with a commit and wait for it to replicate - - # make sure that our project is published to the 'primary' node + # stop other nodes, so we can control which node the commit is sent to praefect_manager.stop_secondary_node praefect_manager.stop_tertiary_node - praefect_manager.wait_for_secondary_node_health_check_failure - praefect_manager.wait_for_tertiary_node_health_check_failure Resource::Repository::ProjectPush.fabricate! do |push| push.project = project push.commit_message = first_added_commit_message push.new_branch = false - push.file_content = "This should exist on all nodes" + push.file_content = 'This file created on gitaly1 while gitaly2/gitaly3 not running' end praefect_manager.start_all_nodes @@ -56,7 +49,7 @@ module QA commit.add_files([ { file_path: "file-#{SecureRandom.hex(8)}", - content: 'This should exist on one node before reconciliation' + content: 'This is created on gitaly2/gitaly3 while gitaly1 is unavailable' } ]) end diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb new file mode 100644 index 00000000000..6e2a34afb3e --- /dev/null +++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_dataloss_spec.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + context 'Praefect dataloss commands', :orchestrated, :gitaly_cluster do + let(:praefect_manager) { Service::PraefectManager.new } + + let(:project) do + Resource::Project.fabricate! do |project| + project.name = 'gitaly_cluster-dataloss-project' + project.initialize_with_readme = true + end + end + + before do + praefect_manager.start_all_nodes + end + + it 'confirms that changes are synced across all storages', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352691' do + expect { praefect_manager.praefect_dataloss_information(project.id) } + .to(eventually_include('All repositories are fully available on all assigned storages!') + .within(max_duration: 60)) + end + + it 'identifies how many changes are not in sync across storages', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352692' do + # Ensure our test repository is replicated and in a consistent state prior to test + praefect_manager.wait_for_project_synced_across_all_storages(project.id) + + # testing for gitaly2 'out of sync' + praefect_manager.stop_secondary_node + + number_of_changes = 3 + 1.upto(number_of_changes) do |i| + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.branch = "newbranch-#{SecureRandom.hex(8)}" + commit.start_branch = project.default_branch + commit.commit_message = 'Add new file' + commit.add_files([ + { file_path: "new_file-#{SecureRandom.hex(8)}.txt", content: 'new file' } + ]) + end + end + + # testing for gitaly3 'in sync' but marked unhealthy + praefect_manager.stop_tertiary_node + + project_data_loss = praefect_manager.praefect_dataloss_information(project.id) + aggregate_failures "validate dataloss identified" do + expect(project_data_loss).to include('gitaly1, assigned host') + expect(project_data_loss).to include("gitaly2 is behind by #{number_of_changes} changes or less, assigned host, unhealthy") + expect(project_data_loss).to include('gitaly3, assigned host, unhealthy') + end + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb index e7e23124312..d066953d12e 100644 --- a/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb +++ b/qa/qa/specs/features/api/3_create/gitaly/praefect_replication_queue_spec.rb @@ -4,7 +4,7 @@ require 'parallel' module QA RSpec.describe 'Create' do - context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/346453', type: :flaky } do + context 'Gitaly Cluster replication queue', :orchestrated, :gitaly_cluster, :skip_live_env do let(:praefect_manager) { Service::PraefectManager.new } let(:project) do Resource::Project.fabricate! do |project| @@ -15,12 +15,10 @@ module QA before do praefect_manager.start_all_nodes - praefect_manager.start_praefect end after do praefect_manager.start_all_nodes - praefect_manager.start_praefect praefect_manager.clear_replication_queue end diff --git a/qa/qa/specs/features/api/3_create/integrations/webhook_events_spec.rb b/qa/qa/specs/features/api/3_create/integrations/webhook_events_spec.rb new file mode 100644 index 00000000000..7a277d754c9 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/integrations/webhook_events_spec.rb @@ -0,0 +1,129 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'WebHooks integration', :requires_admin, :integrations, :orchestrated do + before(:context) do + toggle_local_requests(true) + end + + after(:context) do + Vendor::Smocker::SmockerApi.teardown! + end + + let(:session) { SecureRandom.hex(5) } + + it 'sends a push event', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348945' do + setup_webhook(push: true) do |webhook, smocker| + Resource::Repository::ProjectPush.fabricate! do |project_push| + project_push.project = webhook.project + end + + wait_until do + !smocker.history(session).empty? + end + + events = smocker.history(session).map(&:as_hook_event) + aggregate_failures do + expect(events.size).to be(1), "Should have 1 event: \n#{events.map(&:raw).join("\n")}" + expect(events[0].project_name).to eql(webhook.project.name) + expect(events[0].push?).to be(true), "Not push event: \n#{events[0].raw}" + end + end + end + + it 'sends a merge request event', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349720' do + setup_webhook(merge_requests: true) do |webhook, smocker| + Resource::MergeRequest.fabricate_via_api! do |merge_request| + merge_request.project = webhook.project + end + + wait_until do + !smocker.history(session).empty? + end + + events = smocker.history(session).map(&:as_hook_event) + aggregate_failures do + expect(events.size).to be(1), "Should have 1 event: \n#{events.map(&:raw).join("\n")}" + expect(events[0].project_name).to eql(webhook.project.name) + expect(events[0].mr?).to be(true), "Not MR event: \n#{events[0].raw}" + end + end + end + + it 'sends a wiki page event', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349722' do + setup_webhook(wiki_page: true) do |webhook, smocker| + Resource::Wiki::ProjectPage.fabricate_via_api! do |page| + page.project = webhook.project + end + + wait_until do + !smocker.history(session).empty? + end + + events = smocker.history(session).map(&:as_hook_event) + aggregate_failures do + expect(events.size).to be(1), "Should have 1 event: \n#{events.map(&:raw).join("\n")}" + expect(events[0].project_name).to eql(webhook.project.name) + expect(events[0].wiki?).to be(true), "Not wiki event: \n#{events[0].raw}" + end + end + end + + it 'sends an issues and note event', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349723' do + setup_webhook(issues: true, note: true) do |webhook, smocker| + issue = Resource::Issue.fabricate_via_api! do |issue_init| + issue_init.project = webhook.project + end + + Resource::ProjectIssueNote.fabricate_via_api! do |note| + note.project = issue.project + note.issue = issue + end + + wait_until do + smocker.history(session).size > 1 + end + + events = smocker.history(session).map(&:as_hook_event) + aggregate_failures do + issue_event = events.find(&:issue?) + note_event = events.find(&:note?) + + expect(events.size).to be(2), "Should have 2 events: \n#{events.map(&:raw).join("\n")}" + expect(issue_event).not_to be(nil), "Not issue event: \n#{events[0].raw}" + expect(note_event).not_to be(nil), "Not note event: \n#{events[1].raw}" + end + end + end + + private + + def setup_webhook(**event_args) + Vendor::Smocker::SmockerApi.init(wait: 10) do |smocker| + smocker.register(session: session) + + webhook = Resource::ProjectWebHook.fabricate_via_api! do |hook| + hook.url = smocker.url + + event_args.each do |event, bool| + hook.send("#{event}_events=", bool) + end + end + + yield(webhook, smocker) + + smocker.reset + end + end + + def toggle_local_requests(on) + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: on) + end + + def wait_until(timeout = 120, &block) + Support::Waiter.wait_until(max_duration: timeout, reload_page: false, raise_on_failure: false, &block) + end + end + end +end diff --git a/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb b/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb index 83dcb163d56..6eb3060fb59 100644 --- a/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb +++ b/qa/qa/specs/features/api/3_create/merge_request/push_options_mwps_spec.rb @@ -68,9 +68,10 @@ module QA mr.iid = merge_request[:iid] end - expect(merge_request.state).to eq('opened') - expect(merge_request.merge_status).to eq('checking') - expect(merge_request.merge_when_pipeline_succeeds).to be true + aggregate_failures do + expect(merge_request.state).to eq('opened') + expect(merge_request.merge_when_pipeline_succeeds).to be true + end end it 'merges when pipeline succeeds', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347842' do diff --git a/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_default_enabled_spec.rb b/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_default_enabled_spec.rb index ecc59aa7cc8..bb4e0d71710 100644 --- a/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_default_enabled_spec.rb +++ b/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_default_enabled_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Service ping default enabled' do - context 'When using default enabled from gitlab.yml config', :requires_admin do + context 'When using default enabled from gitlab.yml config', :requires_admin, except: { job: 'review-qa-*' } do before do Flow::Login.sign_in_as_admin @@ -10,7 +10,7 @@ module QA Page::Admin::Menu.perform(&:go_to_metrics_and_profiling_settings) end - it 'has service ping toggle enabled' do + it 'has service ping toggle enabled', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348335' do Page::Admin::Settings::MetricsAndProfiling.perform do |setting| setting.expand_usage_statistics do |page| expect(page).not_to have_disabled_usage_data_checkbox diff --git a/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_disabled_spec.rb b/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_disabled_spec.rb index 309369265c9..cab8bd367f5 100644 --- a/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_disabled_spec.rb +++ b/qa/qa/specs/features/browser_ui/14_non_devops/service_ping_disabled_spec.rb @@ -10,7 +10,7 @@ module QA Page::Admin::Menu.perform(&:go_to_metrics_and_profiling_settings) end - it 'has service ping toggle is disabled' do + it 'has service ping toggle is disabled', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348340' do Page::Admin::Settings::MetricsAndProfiling.perform do |settings| settings.expand_usage_statistics do |usage_statistics| expect(usage_statistics).to have_disabled_usage_data_checkbox diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/gitlab_migration_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/gitlab_migration_group_spec.rb index a18e22f52f1..a1b9e232e3d 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/group/gitlab_migration_group_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/group/gitlab_migration_group_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - describe 'Manage', :requires_admin do + describe 'Manage', :requires_admin, :reliable do describe 'Gitlab migration' do let!(:admin_api_client) { Runtime::API::Client.as_admin } let!(:user) do diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb index 881bc5bc7c3..2db93ac60ea 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/group/transfer_group_spec.rb @@ -31,7 +31,7 @@ module QA testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347692' do Page::Group::Menu.perform(&:click_group_general_settings_item) Page::Group::Settings::General.perform do |general| - general.transfer_group(target_group.path) + general.transfer_group(target_group.path, sub_group_for_transfer.path) sub_group_for_transfer.sandbox = target_group sub_group_for_transfer.reload! diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 098c0b3ba63..5487ecff028 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb @@ -64,7 +64,9 @@ module QA Page::Profile::Accounts::Show.perform do |show| show.delete_account(user.password) end - Support::Waiter.wait_until { !user.exists? } + + # TODO: Remove retry_on_exception once https://gitlab.com/gitlab-org/gitlab/-/issues/24294 is resolved + Support::Waiter.wait_until(retry_on_exception: true, sleep_interval: 3) { !user.exists? } end it 'allows recreating with same credentials', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347868' do diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb index 895027a588d..bfb810b5c2b 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/add_project_member_spec.rb @@ -1,11 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :requires_admin, quarantine: { - issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/350598', - type: :needs_update, - only: { subdomain: :staging } - } do + RSpec.describe 'Manage', :requires_admin do describe 'Add project member' do before do Runtime::Feature.enable(:invite_members_group_modal) @@ -25,7 +21,7 @@ module QA Page::Project::Menu.perform(&:click_members) Page::Project::Members.perform do |members| members.add_member(user.username) - + members.search_member(user.username) expect(members).to have_content("@#{user.username}") end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/create_project_badge_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_badge_spec.rb new file mode 100644 index 00000000000..2933d580957 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/create_project_badge_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage' do + describe 'Create project badge' do + let(:badge_name) { "project-badge-#{SecureRandom.hex(8)}" } + let(:expected_badge_link_url) { "#{Runtime::Scenario.gitlab_address}/#{project.path_with_namespace}" } + let(:expected_badge_image_url) { "#{Runtime::Scenario.gitlab_address}/#{project.path_with_namespace}/badges/main/pipeline.svg" } + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'badge-test-project' + project.initialize_with_readme = true + end + end + + before do + Flow::Login.sign_in + project.visit! + end + + it 'creates project badge successfully', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/350065' do + Resource::ProjectBadge.fabricate! do |badge| + badge.name = badge_name + end + + Page::Project::Settings::Main.perform do |project_settings| + expect(project_settings).to have_notice('New badge added.') + end + + Page::Component::Badges.perform do |badges| + aggregate_failures do + expect(badges).to have_badge(badge_name) + expect(badges).to have_visible_badge_image_link(expected_badge_link_url) + expect(badges.asset_exists?(expected_badge_image_url)).to be_truthy + end + end + + project.visit! + + Page::Project::Show.perform do |project| + expect(project).to have_visible_badge_image_link(expected_badge_link_url) + expect(project.asset_exists?(expected_badge_image_url)).to be_truthy + end + end + + after do + project&.remove_via_api! + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb index 714c4a2da67..4f9ba579730 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/protected_tags_spec.rb @@ -21,8 +21,8 @@ module QA let(:tag_message) { 'Version 0.0.1' } let(:tag_release_notes) { 'Release It!' } - shared_examples 'successful tag creation' do |user| - it "can be created by #{user}" do + shared_examples 'successful tag creation' do |user, testcase| + it "can be created by #{user}", testcase: testcase do Flow::Login.sign_in(as: send(user)) create_tag_for_project(project, tag_name, tag_message, tag_release_notes) @@ -36,8 +36,8 @@ module QA end end - shared_examples 'unsuccessful tag creation' do |user| - it "cannot be created by an unauthorized #{user}" do + shared_examples 'unsuccessful tag creation' do |user, testcase| + it "cannot be created by an unauthorized #{user}", testcase: testcase do Flow::Login.sign_in(as: send(user)) create_tag_for_project(project, tag_name, tag_message, tag_release_notes) @@ -54,8 +54,8 @@ module QA add_members_to_project(project) end - it_behaves_like 'successful tag creation', :developer_user - it_behaves_like 'successful tag creation', :maintainer_user + it_behaves_like 'successful tag creation', :developer_user, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347930' + it_behaves_like 'successful tag creation', :maintainer_user, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347929' end context 'when protected' do @@ -69,8 +69,8 @@ module QA Page::Main::Menu.perform(&:sign_out) end - it_behaves_like 'unsuccessful tag creation', :developer_user - it_behaves_like 'successful tag creation', :maintainer_user + it_behaves_like 'unsuccessful tag creation', :developer_user, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347927' + it_behaves_like 'successful tag creation', :maintainer_user, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347928' end def create_tag_for_project(project, name, message, release_notes) diff --git a/qa/qa/specs/features/browser_ui/1_manage/user/follow_user_activity_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/user/follow_user_activity_spec.rb index 87b51edef08..11cf4f60a80 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/user/follow_user_activity_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/user/follow_user_activity_spec.rb @@ -5,21 +5,27 @@ module QA describe 'User', :requires_admin do let(:admin_api_client) { Runtime::API::Client.as_admin } - let(:user) do + let(:followed_user_api_client) { Runtime::API::Client.new(:gitlab, user: followed_user) } + + let(:followed_user) do Resource::User.fabricate_via_api! do |user| + user.name = "followed_user_#{SecureRandom.hex(8)}" user.api_client = admin_api_client end end - let(:user_api_client) do - Runtime::API::Client.new(:gitlab, user: user) + let(:following_user) do + Resource::User.fabricate_via_api! do |user| + user.name = "following_user_#{SecureRandom.hex(8)}" + user.api_client = admin_api_client + end end let(:group) do group = QA::Resource::Group.fabricate_via_api! do |group| group.path = "group_for_follow_user_activity_#{SecureRandom.hex(8)}" end - group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) + group.add_member(followed_user, Resource::Members::AccessLevel::MAINTAINER) group end @@ -27,7 +33,7 @@ module QA Resource::Project.fabricate_via_api! do |project| project.name = 'project-for-tags' project.initialize_with_readme = true - project.api_client = user_api_client + project.api_client = followed_user_api_client project.group = group end end @@ -35,14 +41,14 @@ module QA let(:merge_request) do Resource::MergeRequest.fabricate_via_api! do |mr| mr.project = project - mr.api_client = user_api_client + mr.api_client = followed_user_api_client end end let(:issue) do Resource::Issue.fabricate_via_api! do |issue| issue.project = project - issue.api_client = user_api_client + issue.api_client = followed_user_api_client end end @@ -51,19 +57,19 @@ module QA project_issue_note.project = project project_issue_note.issue = issue project_issue_note.body = 'This is a comment' - project_issue_note.api_client = user_api_client + project_issue_note.api_client = followed_user_api_client end end before do # Create both tokens before logging in the first time so that we don't need to log out in the middle of the test admin_api_client.personal_access_token - user_api_client.personal_access_token + followed_user_api_client.personal_access_token end it 'can be followed and their activity seen', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347678' do - Flow::Login.sign_in - page.visit Runtime::Scenario.gitlab_address + "/#{user.username}" + Flow::Login.sign_in(as: following_user) + page.visit Runtime::Scenario.gitlab_address + "/#{followed_user.username}" Page::User::Show.perform(&:click_follow_user_link) expect(page).to have_text("No activities found") @@ -76,7 +82,7 @@ module QA Page::Main::Menu.perform(&:click_user_profile_link) Page::User::Show.perform do |show| show.click_following_link - show.click_user_link(user.username) + show.click_user_link(followed_user.username) aggregate_failures do expect(show).to have_activity('created project') @@ -88,9 +94,10 @@ module QA end after do - project.api_client = admin_api_client - project.remove_via_api! - user.remove_via_api! + project&.api_client = admin_api_client + project&.remove_via_api! + followed_user&.remove_via_api! + following_user&.remove_via_api! end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb index c908b1c46a1..d3662884952 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/milestone/assign_milestone_spec.rb @@ -31,8 +31,8 @@ module QA Flow::Login.sign_in end - shared_examples 'milestone assigned to existing issue' do - it 'is assigned to an existing issue' do + shared_examples 'milestone assigned to existing issue' do |testcase| + it 'is assigned to an existing issue', testcase: testcase do issue.visit! Page::Project::Issue::Show.perform do |existing_issue| @@ -43,8 +43,8 @@ module QA end end - shared_examples 'milestone assigned to new issue' do - it 'is assigned to a new issue' do + shared_examples 'milestone assigned to new issue' do |testcase| + it 'is assigned to a new issue', testcase: testcase do Resource::Issue.fabricate_via_browser_ui! do |new_issue| new_issue.project = project new_issue.milestone = milestone @@ -65,8 +65,8 @@ module QA end end - it_behaves_like 'milestone assigned to existing issue' - it_behaves_like 'milestone assigned to new issue' + it_behaves_like 'milestone assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347964' + it_behaves_like 'milestone assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347965' end context 'Project milestone' do @@ -78,8 +78,8 @@ module QA end end - it_behaves_like 'milestone assigned to existing issue' - it_behaves_like 'milestone assigned to new issue' + it_behaves_like 'milestone assigned to existing issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347962' + it_behaves_like 'milestone assigned to new issue', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347963' end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb index 5f896c7bf10..b7284f972ef 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/add_design_content_spec.rb @@ -12,7 +12,7 @@ module QA Flow::Login.sign_in end - it 'user adds a design and annotates it', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347822' do + it 'user adds a design and annotates it', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/352746', type: :investigating }, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347822' do issue.visit! Page::Project::Issue::Show.perform do |issue| diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb index 9a771919c11..85270791f0f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/merge_when_pipeline_succeeds_spec.rb @@ -20,6 +20,18 @@ module QA end before do + Flow::Login.sign_in + end + + after do + runner&.remove_via_api! + project&.remove_via_api! + end + + it 'merges after pipeline succeeds' do + transient_test = repeat > 1 + + # Push a new pipeline config file Resource::Repository::Commit.fabricate_via_api! do |commit| commit.project = project commit.commit_message = 'Add .gitlab-ci.yml' @@ -30,7 +42,7 @@ module QA content: <<~EOF test: tags: ["runner-for-#{project.name}"] - script: sleep 20 + script: sleep 30 only: - merge_requests EOF @@ -39,17 +51,8 @@ module QA ) end - Flow::Login.sign_in - end - - after do - runner&.remove_via_api! - project&.remove_via_api! - end - - it 'merges after pipeline succeeds' do repeat.times do |i| - QA::Runtime::Logger.info("Transient bug test - Trial #{i}") if repeat > 1 + QA::Runtime::Logger.info("Transient bug test - Trial #{i}") if transient_test branch_name = "mr-test-#{SecureRandom.hex(6)}-#{i}" @@ -68,19 +71,54 @@ module QA merge_request.no_preparation = true end + # Load the page so that the browser is as prepared as possible to display the pipeline in progress when we + # start it. merge_request.visit! + # Push a new file to trigger a new pipeline + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add new file' + commit.branch = branch_name + commit.add_files( + [ + { + file_path: "#{branch_name}-file.md", + content: "file content" + } + ] + ) + end + Page::MergeRequest::Show.perform do |mr| - mr.merge_when_pipeline_succeeds! + mr.refresh + + # Part of the challenge with this test is that the MR widget has many components that could be displayed + # and many errors states that those components could encounter. Most of the time few of those + # possible components will be relevant, so it would be inefficient for this test to check for each of + # them. Instead, we fail on anything but the expected state. + # + # The following method allows us to handle and ignore states (as we find them) that users could safely ignore. + mr.wait_until_ready_to_merge(transient_test: transient_test) + + mr.retry_until(reload: true, message: 'Wait until ready to click MWPS') do + merge_request.reload! + + # Click the MWPS button if we can + break mr.merge_when_pipeline_succeeds! if mr.has_element?(:merge_button, text: 'Merge when pipeline succeeds') + + # But fail if the button is missing because the pipeline is complete + raise "The pipeline already finished before we could click MWPS" if mr.wait_until { project.pipelines.first }[:status] == 'success' - Support::Waiter.wait_until(sleep_interval: 5) do - merge_request = merge_request.reload! - merge_request.state == 'merged' + # Otherwise, if this is not a transient test reload the page and retry + next false unless transient_test end aggregate_failures do - expect(merge_request.merge_when_pipeline_succeeds).to be_truthy - expect(mr.merged?).to be_truthy, "Expected content 'The changes were merged' but it did not appear." + expect { mr.merged? }.to eventually_be_truthy.within(max_duration: 60), "Expected content 'The changes were merged' but it did not appear." + expect(merge_request.reload!.merge_when_pipeline_succeeds).to be_truthy + expect(merge_request.state).to eq('merged') + expect(project.pipelines.last[:status]).to eq('success') end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb index 3da73c8fa72..107d72a9724 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_file_template_spec.rb @@ -18,30 +18,34 @@ module QA file_name: '.gitignore', name: 'Android', api_path: 'gitignores', - api_key: 'Android' + api_key: 'Android', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347659' }, { file_name: '.gitlab-ci.yml', name: 'Julia', api_path: 'gitlab_ci_ymls', - api_key: 'Julia' + api_key: 'Julia', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347658' }, { file_name: 'Dockerfile', name: 'Python', api_path: 'dockerfiles', - api_key: 'Python' + api_key: 'Python', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347660' }, { file_name: 'LICENSE', name: 'Mozilla Public License 2.0', api_path: 'licenses', - api_key: 'mpl-2.0' + api_key: 'mpl-2.0', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347657' } ] templates.each do |template| - it "user adds #{template[:file_name]} via file template #{template[:name]}" do + it "user adds #{template[:file_name]} via file template #{template[:name]}", testcase: template[:testcase] do content = fetch_template_from_api(template[:api_path], template[:api_key]) Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_to_canary_gitaly_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_to_canary_gitaly_spec.rb new file mode 100644 index 00000000000..78abdb94dfe --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_to_canary_gitaly_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create', only: { subdomain: %i[staging staging-canary] } do + describe 'Git push to canary Gitaly node over HTTP' do + it 'pushes to a project using a canary specific Gitaly repository storage', :smoke, :requires_admin, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/351116' do + Flow::Login.sign_in_as_admin + + project = Resource::Project.fabricate_via_api! do |storage_project| + storage_project.name = 'canary-specific-repository-storage' + storage_project.repository_storage = 'nfs-file-cny01' # TODO: move to ENV var + end + + Resource::Repository::Push.fabricate! do |push| + push.repository_http_uri = project.repository_http_location.uri + push.file_name = 'README.md' + push.file_content = "# This is a test project named #{project.name}" + push.commit_message = 'Add README.md' + push.new_branch = true + end + + project.visit! + + Page::Project::Show.perform do |project_page| + expect(project_page).to have_file('README.md') + expect(project_page).to have_readme_content("This is a test project named #{project.name}") + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/add_comment_to_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/add_comment_to_snippet_spec.rb index 6ab50ba56f2..1a7c64a363f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/add_comment_to_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/add_comment_to_snippet_spec.rb @@ -28,8 +28,8 @@ module QA project_snippet&.remove_via_api! end - shared_examples 'comments on snippets' do |snippet_type| - it "adds, edits, and deletes a comment on a #{snippet_type}" do + shared_examples 'comments on snippets' do |snippet_type, testcase| + it "adds, edits, and deletes a comment on a #{snippet_type}", testcase: testcase do send(snippet_type) Page::Main::Menu.perform(&:sign_out) @@ -49,8 +49,8 @@ module QA end end - it_behaves_like 'comments on snippets', :personal_snippet - it_behaves_like 'comments on snippets', :project_snippet + it_behaves_like 'comments on snippets', :personal_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347816' + it_behaves_like 'comments on snippets', :project_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347817' def create_comment Page::Dashboard::Snippet::Show.perform do |snippet| diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb index 72d83eadde9..8f05446ff70 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/add_file_to_snippet_spec.rb @@ -28,8 +28,8 @@ module QA project_snippet&.remove_via_api! end - shared_examples 'adding file to snippet' do |snippet_type| - it "adds second file to an existing #{snippet_type} to make it multi-file" do + shared_examples 'adding file to snippet' do |snippet_type, testcase| + it "adds second file to an existing #{snippet_type} to make it multi-file", testcase: testcase do send(snippet_type).visit! Page::Dashboard::Snippet::Show.perform(&:click_edit_button) @@ -52,8 +52,8 @@ module QA end end - it_behaves_like 'adding file to snippet', :personal_snippet - it_behaves_like 'adding file to snippet', :project_snippet + it_behaves_like 'adding file to snippet', :personal_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347845' + it_behaves_like 'adding file to snippet', :project_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347846' end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb index 29ddbb22a01..0b63d9a1edb 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/copy_snippet_file_contents_spec.rb @@ -59,8 +59,8 @@ module QA project_snippet&.remove_via_api! end - shared_examples 'copying snippet file contents' do |snippet_type| - it "copies file contents of a multi-file #{snippet_type} to a comment and verifies them" do + shared_examples 'copying snippet file contents' do |snippet_type, testcase| + it "copies file contents of a multi-file #{snippet_type} to a comment and verifies them", testcase: testcase do send(snippet_type).visit! files.each do |files| @@ -73,8 +73,8 @@ module QA end end - it_behaves_like 'copying snippet file contents', :personal_snippet - it_behaves_like 'copying snippet file contents', :project_snippet + it_behaves_like 'copying snippet file contents', :personal_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347849' + it_behaves_like 'copying snippet file contents', :project_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347848' end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb index dc66e0c5a9f..e04f580dc15 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/create_personal_snippet_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do # convert back to a smoke test once proved to be stable + RSpec.describe 'Create', :smoke do describe 'Personal snippet creation' do let(:snippet) do Resource::Snippet.fabricate_via_browser_ui! do |snippet| diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb index 014c0ca4d48..b6092ef0c4c 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/delete_file_from_snippet_spec.rb @@ -36,8 +36,8 @@ module QA project_snippet&.remove_via_api! end - shared_examples 'deleting file from snippet' do |snippet_type| - it "deletes second file from an existing #{snippet_type} to make it single-file" do + shared_examples 'deleting file from snippet' do |snippet_type, testcase| + it "deletes second file from an existing #{snippet_type} to make it single-file", testcase: testcase do send(snippet_type).visit! Page::Dashboard::Snippet::Show.perform(&:click_edit_button) @@ -58,8 +58,8 @@ module QA end end - it_behaves_like 'deleting file from snippet', :personal_snippet - it_behaves_like 'deleting file from snippet', :project_snippet + it_behaves_like 'deleting file from snippet', :personal_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347728' + it_behaves_like 'deleting file from snippet', :project_snippet, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347727' end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/snippet_index_page_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/snippet_index_page_spec.rb index d922950335f..97e42edfd01 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/snippet_index_page_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/snippet_index_page_spec.rb @@ -56,8 +56,8 @@ module QA project_snippet_with_multiple_files.remove_via_api! end - shared_examples 'displaying details on index page' do |snippet_type| - it "shows correct details of #{snippet_type} including file number" do + shared_examples 'displaying details on index page' do |snippet_type, testcase| + it "shows correct details of #{snippet_type} including file number", testcase: testcase do send(snippet_type) Page::Main::Menu.perform do |menu| menu.go_to_menu_dropdown_option(:snippets_link) @@ -73,10 +73,10 @@ module QA end end - it_behaves_like 'displaying details on index page', :personal_snippet_with_single_file - it_behaves_like 'displaying details on index page', :personal_snippet_with_multiple_files - it_behaves_like 'displaying details on index page', :project_snippet_with_single_file - it_behaves_like 'displaying details on index page', :project_snippet_with_multiple_files + it_behaves_like 'displaying details on index page', :personal_snippet_with_single_file, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347717' + it_behaves_like 'displaying details on index page', :personal_snippet_with_multiple_files, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347720' + it_behaves_like 'displaying details on index page', :project_snippet_with_single_file, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347718' + it_behaves_like 'displaying details on index page', :project_snippet_with_multiple_files, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347719' end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb index 70c9c9beeb8..19dd868744f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/add_file_template_spec.rb @@ -18,30 +18,34 @@ module QA file_name: '.gitignore', name: 'Android', api_path: 'gitignores', - api_key: 'Android' + api_key: 'Android', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347752' }, { file_name: '.gitlab-ci.yml', name: 'Julia', api_path: 'gitlab_ci_ymls', - api_key: 'Julia' + api_key: 'Julia', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347753' }, { file_name: 'Dockerfile', name: 'Python', api_path: 'dockerfiles', - api_key: 'Python' + api_key: 'Python', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347750' }, { file_name: 'LICENSE', name: 'Mozilla Public License 2.0', api_path: 'licenses', - api_key: 'mpl-2.0' + api_key: 'mpl-2.0', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347751' } ] templates.each do |template| - it "user adds #{template[:file_name]} via file template #{template[:name]}" do + it "user adds #{template[:file_name]} via file template #{template[:name]}", testcase: template[:testcase] do content = fetch_template_from_api(template[:api_path], template[:api_key]) Flow::Login.sign_in diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb index 758aae9f729..96e85139e78 100644 --- a/qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/open_fork_in_web_ide_spec.rb @@ -11,7 +11,7 @@ module QA end context 'when a user does not have permissions to commit to the project' do - let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } + let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) } context 'when no fork is present' do it 'suggests to create a fork when a user clicks Web IDE in the main project', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347823' do @@ -45,6 +45,10 @@ module QA submit_merge_request_upstream end + + after do + fork_project.project.remove_via_api! + end end def submit_merge_request_upstream diff --git a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb index 67eee66b3d6..b45624381c8 100644 --- a/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/wiki/content_editor_spec.rb @@ -1,13 +1,12 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create', :requires_admin, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/350220', type: :investigating } do # remove :requires_admin once the ff is enabled by default in https://gitlab.com/gitlab-org/gitlab/-/issues/345398 + RSpec.describe 'Create' do context 'Content Editor' do let(:initial_wiki) { Resource::Wiki::ProjectPage.fabricate_via_api! } let(:page_title) { 'Content Editor Page' } let(:heading_text) { 'My New Heading' } let(:image_file_name) { 'testfile.png' } - let!(:toggle) { Runtime::Feature.enabled?(:wiki_switch_between_content_editor_raw_markdown) } before do Flow::Login.sign_in @@ -24,7 +23,7 @@ module QA Page::Project::Wiki::Edit.perform do |edit| edit.set_title(page_title) - edit.use_new_editor(toggle) + edit.use_new_editor edit.add_heading('Heading 1', heading_text) edit.upload_image(File.absolute_path(File.join('qa', 'fixtures', 'designs', image_file_name))) end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb index 22bb5fed84c..0bc3fb7b829 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pass_dotenv_variables_to_downstream_via_bridge_spec.rb @@ -25,7 +25,7 @@ module QA Resource::Runner.fabricate! do |runner| runner.name = executor runner.tags = [executor] - runner.token = group.sandbox.runners_token + runner.token = group.reload!.runners_token end end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb new file mode 100644 index 00000000000..7a2c2b4ae90 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/run_pipeline_with_manual_jobs_spec.rb @@ -0,0 +1,98 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Verify', :runner, quarantine: { + type: :flaky, + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/351994' + } do + describe 'Run pipeline with manual jobs' do + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'pipeline-with-manual-job' + project.description = 'Project for pipeline with manual job' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = "qa-runner-#{SecureRandom.hex(3)}" + end + end + + let!(:ci_file) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files( + [ + { + file_path: '.gitlab-ci.yml', + content: <<~YAML + stages: + - Stage1 + - Stage2 + - Stage3 + + Prep: + stage: Stage1 + script: exit 0 + when: manual + + Build: + stage: Stage2 + needs: ['Prep'] + script: exit 0 + parallel: 6 + + Test: + stage: Stage3 + needs: ['Build'] + script: exit 0 + + Deploy: + stage: Stage3 + needs: ['Test'] + script: exit 0 + parallel: 6 + YAML + } + ] + ) + end + end + + before do + Flow::Login.sign_in + project.visit! + Flow::Pipeline.visit_latest_pipeline(pipeline_condition: 'skipped') + end + + after do + runner&.remove_via_api! + project&.remove_via_api! + end + + it 'does not leave any job in skipped state', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349158' do + Page::Project::Pipeline::Show.perform do |show| + show.click_job_action('Prep') # Trigger pipeline manually + + show.wait_until(max_duration: 300, sleep_interval: 2, reload: false) do + project.pipelines.last[:status] == 'success' + end + + aggregate_failures do + expect(show).to have_build('Test', status: :success) + + show.click_job_dropdown('Build') + expect(show).not_to have_skipped_job_in_group + + show.click_job_dropdown('Build') # Close Build dropdown + show.click_job_dropdown('Deploy') + expect(show).not_to have_skipped_job_in_group + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb index 5e0f1911811..9a5a5416d5c 100644 --- a/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/dependency_proxy/dependency_proxy_spec.rb @@ -3,6 +3,8 @@ module QA RSpec.describe 'Package', :orchestrated, :registry, only: { pipeline: :main } do describe 'Dependency Proxy' do + using RSpec::Parameterized::TableSyntax + let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'dependency-proxy-project' @@ -40,12 +42,13 @@ module QA runner.remove_via_api! end - where(:docker_client_version) do - %w[docker:19.03.12 docker:20.10] + where(:case_name, :docker_client_version, :testcase) do + 'using docker:19.03.12' | 'docker:19.03.12' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347605' + 'using docker:20.10' | 'docker:20.10' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347604' end with_them do - it "pulls an image using the dependency proxy" do + it "pulls an image using the dependency proxy", testcase: params[:testcase] do Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| commit.project = project @@ -58,7 +61,7 @@ module QA image: "#{docker_client_version}" services: - name: "#{docker_client_version}-dind" - command: ["--insecure-registry=gitlab.test:80"] + command: ["--insecure-registry=gitlab.test:80"] before_script: - apk add curl jq grep - echo $CI_DEPENDENCY_PROXY_SERVER diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb index 92e4d64fee4..2da0f6a0cf8 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/composer_registry_spec.rb @@ -32,55 +32,22 @@ module QA "#{uri.scheme}://#{uri.host}:#{uri.port}" end - let(:composer_json_file) do - <<~EOF - { - "name": "#{project.path_with_namespace}/#{package.name}", - "description": "Library XY", - "type": "library", - "license": "GPL-3.0-only", - "authors": [ - { - "name": "John Doe", - "email": "john@example.com" - } - ], - "require": {} - } - EOF - end - - let(:gitlab_ci_yaml) do - <<~YAML - publish: - image: curlimages/curl:latest - stage: build - variables: - URL: "$CI_SERVER_PROTOCOL://$CI_SERVER_HOST:$CI_SERVER_PORT/api/v4/projects/$CI_PROJECT_ID/packages/composer?job_token=$CI_JOB_TOKEN" - script: - - version=$([[ -z "$CI_COMMIT_TAG" ]] && echo "branch=$CI_COMMIT_REF_NAME" || echo "tag=$CI_COMMIT_TAG") - - insecure=$([ "$CI_SERVER_PROTOCOL" = "http" ] && echo "--insecure" || echo "") - - response=$(curl -s -w "%{http_code}" $insecure --data $version $URL) - - code=$(echo "$response" | tail -n 1) - - body=$(echo "$response" | head -n 1) - tags: - - "runner-for-#{project.name}" - YAML - end - before do Flow::Login.sign_in Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + composer_yaml = ERB.new(read_fixture('package_managers/composer', 'composer_upload_package.yaml.erb')).result(binding) + composer_json = ERB.new(read_fixture('package_managers/composer', 'composer.json.erb')).result(binding) + commit.project = project - commit.commit_message = 'Add .gitlab-ci.yml' + commit.commit_message = 'Add files' commit.add_files([{ file_path: '.gitlab-ci.yml', - content: gitlab_ci_yaml + content: composer_yaml }, { file_path: 'composer.json', - content: composer_json_file + content: composer_json }] ) end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb index 15578cd5e6b..22495796605 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/conan_repository_spec.rb @@ -46,25 +46,13 @@ module QA Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + conan_yaml = ERB.new(read_fixture('package_managers/conan', 'conan_upload_install_package.yaml.erb')).result(binding) + commit.project = project commit.commit_message = 'Add .gitlab-ci.yml' commit.add_files([{ file_path: '.gitlab-ci.yml', - content: - <<~YAML - image: conanio/gcc7 - - test_package: - stage: deploy - script: - - "conan remote add gitlab #{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/conan" - - "conan new #{package.name}/0.1 -t" - - "conan create . mycompany/stable" - - "CONAN_LOGIN_USERNAME=ci_user CONAN_PASSWORD=${CI_JOB_TOKEN} conan upload #{package.name}/0.1@mycompany/stable --all --remote=gitlab" - - "conan install #{package.name}/0.1@mycompany/stable --remote=gitlab" - tags: - - "runner-for-#{project.name}" - YAML + content: conan_yaml }]) end end @@ -90,8 +78,10 @@ module QA Page::Project::Packages::Show.perform(&:click_delete) Page::Project::Packages::Index.perform do |index| - expect(index).to have_content("Package deleted successfully") - expect(index).not_to have_package(package.name) + aggregate_failures 'package deletion' do + expect(index).to have_content("Package deleted successfully") + expect(index).not_to have_package(package.name) + end end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb index ded90607d67..71acc3a8f92 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/generic_repository_spec.rb @@ -3,6 +3,8 @@ module QA RSpec.describe 'Package', :orchestrated, :packages, :object_storage do describe 'Generic Repository' do + include Runtime::Fixtures + let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'generic-package-project' @@ -25,29 +27,6 @@ module QA end end - let(:gitlab_ci_yaml) do - <<~YAML - image: curlimages/curl:latest - - stages: - - upload - - download - - upload: - stage: upload - script: - - 'curl --header "JOB-TOKEN: $CI_JOB_TOKEN" --upload-file file.txt ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/#{package.name}/0.0.1/file.txt' - tags: - - "runner-for-#{project.name}" - download: - stage: download - script: - - 'wget --header="JOB-TOKEN: $CI_JOB_TOKEN" ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/generic/#{package.name}/0.0.1/file.txt -O file_downloaded.txt' - tags: - - "runner-for-#{project.name}" - YAML - end - let(:file_txt) do <<~EOF Hello, world! @@ -59,11 +38,13 @@ module QA Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + generic_packages_yaml = ERB.new(read_fixture('package_managers/generic', 'generic_upload_install_package.yaml.erb')).result(binding) + commit.project = project - commit.commit_message = 'Add .gitlab-ci.yml' + commit.commit_message = 'Add files' commit.add_files([{ file_path: '.gitlab-ci.yml', - content: gitlab_ci_yaml + content: generic_packages_yaml }, { file_path: 'file.txt', @@ -100,21 +81,11 @@ module QA package.remove_via_api! end - it 'uploads a generic package, downloads and deletes it', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348017' do + it 'uploads a generic package and downloads it', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348017' do Page::Project::Menu.perform(&:click_packages_link) Page::Project::Packages::Index.perform do |index| expect(index).to have_package(package.name) - index.click_package(package.name) - end - - Page::Project::Packages::Show.perform(&:click_delete) - - Page::Project::Packages::Index.perform do |index| - aggregate_failures 'package deletion' do - expect(index).to have_content("Package deleted successfully") - expect(index).to have_no_package(package.name) - end end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb index 92d0f547764..d2e816f9bf9 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/helm_registry_spec.rb @@ -3,6 +3,7 @@ module QA RSpec.describe 'Package', :orchestrated, :packages, :object_storage do describe 'Helm Registry' do + using RSpec::Parameterized::TableSyntax include Runtime::Fixtures include_context 'packages registry qa scenario' @@ -10,140 +11,105 @@ module QA let(:package_version) { '1.3.7' } let(:package_type) { 'helm' } - let(:package_gitlab_ci_file) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - deploy: - image: alpine:3 - script: - - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing - - apk add curl - - helm create #{package_name} - - cp ./Chart.yaml #{package_name} - - helm package #{package_name} - - http_code=$(curl --write-out "%{http_code}" --request POST --form 'chart=@#{package_name}-#{package_version}.tgz' --user #{username}:#{access_token} ${CI_API_V4_URL}/projects/${CI_PROJECT_ID}/packages/helm/api/stable/charts --output /dev/null --silent) - - '[ $http_code = "201" ]' - only: - - "#{package_project.default_branch}" - tags: - - "runner-for-#{package_project.group.name}" - YAML - } + where(:case_name, :authentication_token_type, :testcase) do + 'using personal access token' | :personal_access_token | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347586' + 'using ci job token' | :ci_job_token | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347587' + 'using project deploy token' | :project_deploy_token | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347588' end - let(:package_chart_yaml_file) do - { - file_path: "Chart.yaml", - content: - <<~EOF - apiVersion: v2 - name: #{package_name} - description: GitLab QA helm package - type: application - version: #{package_version} - appVersion: "1.16.0" - EOF - } - end - - let(:client_gitlab_ci_file) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - pull: - image: alpine:3 - script: - - apk add helm --repository=http://dl-cdn.alpinelinux.org/alpine/edge/testing - - helm repo add --username #{username} --password #{access_token} gitlab_qa ${CI_API_V4_URL}/projects/#{package_project.id}/packages/helm/stable - - helm repo update - - helm pull gitlab_qa/#{package_name} - only: - - "#{client_project.default_branch}" - tags: - - "runner-for-#{client_project.group.name}" - YAML - } - end - - %i[personal_access_token ci_job_token project_deploy_token].each do |authentication_token_type| - context "using a #{authentication_token_type}" do - let(:username) do - case authentication_token_type - when :personal_access_token - Runtime::User.username - when :ci_job_token - 'gitlab-ci-token' - when :project_deploy_token - project_deploy_token.username - end + with_them do + let(:username) do + case authentication_token_type + when :personal_access_token + Runtime::User.username + when :ci_job_token + 'gitlab-ci-token' + when :project_deploy_token + project_deploy_token.username end + end - let(:access_token) do - case authentication_token_type - when :personal_access_token - personal_access_token - when :ci_job_token - '${CI_JOB_TOKEN}' - when :project_deploy_token - project_deploy_token.token - end + let(:access_token) do + case authentication_token_type + when :personal_access_token + personal_access_token + when :ci_job_token + '${CI_JOB_TOKEN}' + when :project_deploy_token + project_deploy_token.token end + end - it "pushes and pulls a helm chart" do - Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = package_project - commit.commit_message = 'Add .gitlab-ci.yml' - commit.add_files([package_gitlab_ci_file, package_chart_yaml_file]) - end + it "pushes and pulls a helm chart", testcase: params[:testcase] do + Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + helm_upload_yaml = ERB.new(read_fixture('package_managers/helm', 'helm_upload_package.yaml.erb')).result(binding) + helm_chart_yaml = ERB.new(read_fixture('package_managers/helm', 'Chart.yaml.erb')).result(binding) + + commit.project = package_project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files([ + { + file_path: '.gitlab-ci.yml', + content: helm_upload_yaml + }, + { + file_path: 'Chart.yaml', + content: helm_chart_yaml + } + ]) end + end - package_project.visit! + package_project.visit! - Flow::Pipeline.visit_latest_pipeline + Flow::Pipeline.visit_latest_pipeline - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('deploy') - end + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('deploy') + end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 800) - end + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 800) + end - Page::Project::Menu.perform(&:click_packages_link) + Page::Project::Menu.perform(&:click_packages_link) - Page::Project::Packages::Index.perform do |index| - expect(index).to have_package(package_name) + Page::Project::Packages::Index.perform do |index| + expect(index).to have_package(package_name) - index.click_package(package_name) - end + index.click_package(package_name) + end - Page::Project::Packages::Show.perform do |show| - expect(show).to have_package_info(package_name, package_version) - end + Page::Project::Packages::Show.perform do |show| + expect(show).to have_package_info(package_name, package_version) + end - Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do - Resource::Repository::Commit.fabricate_via_api! do |commit| - commit.project = client_project - commit.commit_message = 'Add .gitlab-ci.yml' - commit.add_files([client_gitlab_ci_file]) - end + Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do + Resource::Repository::Commit.fabricate_via_api! do |commit| + helm_install_yaml = ERB.new(read_fixture('package_managers/helm', 'helm_install_package.yaml.erb')).result(binding) + + commit.project = client_project + commit.commit_message = 'Add .gitlab-ci.yml' + commit.add_files([ + { + file_path: '.gitlab-ci.yml', + content: helm_install_yaml + } + ]) end + end - client_project.visit! + client_project.visit! - Flow::Pipeline.visit_latest_pipeline + Flow::Pipeline.visit_latest_pipeline - Page::Project::Pipeline::Show.perform do |pipeline| - pipeline.click_job('pull') - end + Page::Project::Pipeline::Show.perform do |pipeline| + pipeline.click_job('pull') + end - Page::Project::Job::Show.perform do |job| - expect(job).to be_successful(timeout: 800) - end + Page::Project::Job::Show.perform do |job| + expect(job).to be_successful(timeout: 800) end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb index 57e1aa6a087..45693ecee41 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_gradle_repository_spec.rb @@ -13,80 +13,10 @@ module QA let(:package_version) { '1.3.7' } let(:package_type) { 'maven_gradle' } - let(:package_gitlab_ci_file) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - deploy: - image: gradle:6.5-jdk11 - script: - - 'gradle publish' - only: - - "#{package_project.default_branch}" - tags: - - "runner-for-#{package_project.group.name}" - YAML - } - end - - let(:package_build_gradle_file) do - { - file_path: 'build.gradle', - content: - <<~EOF - plugins { - id 'java' - id 'maven-publish' - } - - publishing { - publications { - library(MavenPublication) { - groupId '#{group_id}' - artifactId '#{artifact_id}' - version '#{package_version}' - from components.java - } - } - repositories { - maven { - url "#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven" - credentials(HttpHeaderCredentials) { - name = "Private-Token" - value = "#{personal_access_token}" - } - authentication { - header(HttpHeaderAuthentication) - } - } - } - } - EOF - } - end - - let(:client_gitlab_ci_file) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - build: - image: gradle:6.5-jdk11 - script: - - 'gradle build' - only: - - "#{client_project.default_branch}" - tags: - - "runner-for-#{client_project.group.name}" - YAML - } - end - - where(:authentication_token_type, :maven_header_name) do - :personal_access_token | 'Private-Token' - :ci_job_token | 'Job-Token' - :project_deploy_token | 'Deploy-Token' + where(:case_name, :authentication_token_type, :maven_header_name, :testcase) do + 'using personal access token' | :personal_access_token | 'Private-Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347601' + 'using ci job token' | :ci_job_token | 'Job-Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347603' + 'using project deploy token' | :project_deploy_token | 'Deploy-Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347602' end with_them do @@ -101,49 +31,24 @@ module QA end end - let(:client_build_gradle_file) do - { - file_path: 'build.gradle', - content: - <<~EOF - plugins { - id 'java' - id 'application' - } - - repositories { - jcenter() - maven { - url "#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven" - name "GitLab" - credentials(HttpHeaderCredentials) { - name = '#{maven_header_name}' - value = #{token} - } - authentication { - header(HttpHeaderAuthentication) - } - } - } - - dependencies { - implementation group: '#{group_id}', name: '#{artifact_id}', version: '#{package_version}' - testImplementation 'junit:junit:4.12' - } - - application { - mainClassName = 'gradle_maven_app.App' - } - EOF - } - end - - it "pushes and pulls a maven package via gradle using #{params[:authentication_token_type]}" do + it 'pushes and pulls a maven package via gradle', testcase: params[:testcase] do Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + gradle_upload_yaml = ERB.new(read_fixture('package_managers/maven', 'gradle_upload_package.yaml.erb')).result(binding) + build_upload_gradle = ERB.new(read_fixture('package_managers/maven', 'build_upload.gradle.erb')).result(binding) + commit.project = package_project commit.commit_message = 'Add .gitlab-ci.yml' - commit.add_files([package_gitlab_ci_file, package_build_gradle_file]) + commit.add_files([ + { + file_path: '.gitlab-ci.yml', + content: gradle_upload_yaml + }, + { + file_path: 'build.gradle', + content: build_upload_gradle + } + ]) end end @@ -173,9 +78,21 @@ module QA Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + gradle_install_yaml = ERB.new(read_fixture('package_managers/maven', 'gradle_install_package.yaml.erb')).result(binding) + build_install_gradle = ERB.new(read_fixture('package_managers/maven', 'build_install.gradle.erb')).result(binding) + commit.project = client_project - commit.commit_message = 'Add .gitlab-ci.yml' - commit.add_files([client_gitlab_ci_file, client_build_gradle_file]) + commit.commit_message = 'Add files' + commit.add_files([ + { + file_path: '.gitlab-ci.yml', + content: gradle_install_yaml + }, + { + file_path: 'build.gradle', + content: build_install_gradle + } + ]) end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb index e6591b6adb9..b4ebb9dd475 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven_repository_spec.rb @@ -13,121 +13,6 @@ module QA let(:package_version) { '1.3.7' } let(:package_type) { 'maven' } - let(:package_gitlab_ci_file) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - deploy: - image: maven:3.6-jdk-11 - script: - - 'mvn deploy -s settings.xml' - only: - - "#{package_project.default_branch}" - tags: - - "runner-for-#{package_project.group.name}" - YAML - } - end - - let(:package_pom_file) do - { - file_path: 'pom.xml', - content: <<~XML - <project> - <groupId>#{group_id}</groupId> - <artifactId>#{artifact_id}</artifactId> - <version>#{package_version}</version> - <modelVersion>4.0.0</modelVersion> - <repositories> - <repository> - <id>#{package_project.name}</id> - <url>#{gitlab_address_with_port}/api/v4/groups/#{package_project.group.id}/-/packages/maven</url> - </repository> - </repositories> - <distributionManagement> - <repository> - <id>#{package_project.name}</id> - <url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url> - </repository> - <snapshotRepository> - <id>#{package_project.name}</id> - <url>#{gitlab_address_with_port}/api/v4/projects/#{package_project.id}/packages/maven</url> - </snapshotRepository> - </distributionManagement> - </project> - XML - } - end - - let(:client_gitlab_ci_file) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - install: - image: maven:3.6-jdk-11 - script: - - "mvn install -s settings.xml" - only: - - "#{client_project.default_branch}" - tags: - - "runner-for-#{client_project.group.name}" - YAML - } - end - - let(:client_pom_file) do - { - file_path: 'pom.xml', - content: <<~XML - <project> - <groupId>#{group_id}</groupId> - <artifactId>maven_client</artifactId> - <version>1.0</version> - <modelVersion>4.0.0</modelVersion> - <repositories> - <repository> - <id>#{package_project.name}</id> - <url>#{gitlab_address_with_port}/api/v4/groups/#{package_project.group.id}/-/packages/maven</url> - </repository> - </repositories> - <dependencies> - <dependency> - <groupId>#{group_id}</groupId> - <artifactId>#{artifact_id}</artifactId> - <version>#{package_version}</version> - </dependency> - </dependencies> - </project> - XML - } - end - - let(:settings_xml_with_pat) do - { - file_path: 'settings.xml', - content: <<~XML - <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> - <servers> - <server> - <id>#{package_project.name}</id> - <configuration> - <httpHeaders> - <property> - <name>Private-Token</name> - <value>#{personal_access_token}</value> - </property> - </httpHeaders> - </configuration> - </server> - </servers> - </settings> - XML - } - end - where(:authentication_token_type, :maven_header_name) do :personal_access_token | 'Private-Token' :ci_job_token | 'Job-Token' @@ -146,39 +31,28 @@ module QA end end - let(:settings_xml) do - { - file_path: 'settings.xml', - content: <<~XML - <settings xmlns="http://maven.apache.org/SETTINGS/1.1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" - xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.1.0 http://maven.apache.org/xsd/settings-1.1.0.xsd"> - <servers> - <server> - <id>#{package_project.name}</id> - <configuration> - <httpHeaders> - <property> - <name>#{maven_header_name}</name> - <value>#{token}</value> - </property> - </httpHeaders> - </configuration> - </server> - </servers> - </settings> - XML - } - end - it "pushes and pulls a maven package via maven using #{params[:authentication_token_type]}" do Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + maven_upload_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_upload_package.yaml.erb')).result(binding) + package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding) + settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding) + commit.project = package_project - commit.commit_message = 'Add .gitlab-ci.yml' + commit.commit_message = 'Add files' commit.add_files([ - package_gitlab_ci_file, - package_pom_file, - settings_xml + { + file_path: '.gitlab-ci.yml', + content: maven_upload_package_yaml + }, + { + file_path: 'pom.xml', + content: package_pom_xml + }, + { + file_path: 'settings.xml', + content: settings_xml + } ]) end end @@ -209,12 +83,25 @@ module QA Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + maven_install_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_install_package.yaml.erb')).result(binding) + client_pom_xml = ERB.new(read_fixture('package_managers/maven', 'client_pom.xml.erb')).result(binding) + settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding) + commit.project = client_project - commit.commit_message = 'Add .gitlab-ci.yml' + commit.commit_message = 'Add files' commit.add_files([ - client_gitlab_ci_file, - client_pom_file, - settings_xml + { + file_path: '.gitlab-ci.yml', + content: maven_install_package_yaml + }, + { + file_path: 'pom.xml', + content: client_pom_xml + }, + { + file_path: 'settings.xml', + content: settings_xml + } ]) end end @@ -278,7 +165,19 @@ module QA end def create_duplicated_package - with_fixtures([package_pom_file, settings_xml_with_pat]) do |dir| + settings_xml_with_pat = ERB.new(read_fixture('package_managers/maven', 'settings_with_pat.xml.erb')).result(binding) + package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding) + + with_fixtures([ + { + file_path: 'pom.xml', + content: package_pom_xml + }, + { + file_path: 'settings.xml', + content: settings_xml_with_pat + } + ]) do |dir| Service::DockerRun::Maven.new(dir).publish! end @@ -294,12 +193,25 @@ module QA def push_duplicated_package Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + maven_upload_package_yaml = ERB.new(read_fixture('package_managers/maven', 'maven_upload_package.yaml.erb')).result(binding) + package_pom_xml = ERB.new(read_fixture('package_managers/maven', 'package_pom.xml.erb')).result(binding) + settings_xml = ERB.new(read_fixture('package_managers/maven', 'settings.xml.erb')).result(binding) + commit.project = client_project commit.commit_message = 'Add .gitlab-ci.yml' commit.add_files([ - package_gitlab_ci_file, - package_pom_file, - settings_xml + { + file_path: '.gitlab-ci.yml', + content: maven_upload_package_yaml + }, + { + file_path: 'pom.xml', + content: package_pom_xml + }, + { + file_path: 'settings.xml', + content: settings_xml + } ]) end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb index 70b31c1beca..04aaefbaf5c 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_instance_level_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package Registry', :orchestrated, :packages, :object_storage do + RSpec.describe 'Package Registry', :orchestrated, :reliable, :packages, :object_storage do describe 'npm instance level endpoint' do using RSpec::Parameterized::TableSyntax include Runtime::Fixtures @@ -50,79 +50,10 @@ module QA runner.name = "qa-runner-#{Time.now.to_i}" runner.tags = ["runner-for-#{project.group.name}"] runner.executor = :docker - runner.token = project.group.runners_token + runner.token = project.group.reload!.runners_token end end - let(:gitlab_ci_deploy_yaml) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - image: node:latest - - stages: - - deploy - - deploy: - stage: deploy - script: - - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=#{auth_token}">.npmrc - - npm publish - only: - - "#{project.default_branch}" - tags: - - "runner-for-#{project.group.name}" - YAML - } - end - - let(:gitlab_ci_install_yaml) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - image: node:latest - - stages: - - install - - install: - stage: install - script: - - "npm config set @#{registry_scope}:registry #{gitlab_address_with_port}/api/v4/packages/npm/" - - "npm install #{package.name}" - cache: - key: ${CI_BUILD_REF_NAME} - paths: - - node_modules/ - artifacts: - paths: - - node_modules/ - only: - - "#{another_project.default_branch}" - tags: - - "runner-for-#{another_project.group.name}" - YAML - } - end - - let(:package_json) do - { - file_path: 'package.json', - content: <<~JSON - { - "name": "#{package.name}", - "version": "1.0.0", - "description": "Example package for GitLab npm registry", - "publishConfig": { - "@#{registry_scope}:registry": "#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/npm/" - } - } - JSON - } - end - let(:package) do Resource::Package.init do |package| package.name = "@#{registry_scope}/#{project.name}-#{SecureRandom.hex(8)}" @@ -137,10 +68,10 @@ module QA another_project.remove_via_api! end - where(:authentication_token_type, :token_name) do - :personal_access_token | 'Personal Access Token' - :ci_job_token | 'CI Job Token' - :project_deploy_token | 'Deploy Token' + where(:case_name, :authentication_token_type, :token_name, :testcase) do + 'using personal access token' | :personal_access_token | 'Personal Access Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347600' + 'using ci job token' | :ci_job_token | 'CI Job Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347599' + 'using project deploy token' | :project_deploy_token | 'Deploy Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347598' end with_them do @@ -155,14 +86,23 @@ module QA end end - it "push and pull a npm package via CI using a #{params[:token_name]}" do + it 'push and pull a npm package via CI', testcase: params[:testcase] do Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do + npm_upload_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_upload_package_instance.yaml.erb')).result(binding) + package_json = ERB.new(read_fixture('package_managers/npm', 'package_instance.json.erb')).result(binding) + Resource::Repository::Commit.fabricate_via_api! do |commit| commit.project = project - commit.commit_message = 'Add .gitlab-ci.yml' + commit.commit_message = 'Add files' commit.add_files([ - gitlab_ci_deploy_yaml, - package_json + { + file_path: '.gitlab-ci.yml', + content: npm_upload_yaml + }, + { + file_path: 'package.json', + content: package_json + } ]) end end @@ -180,10 +120,15 @@ module QA Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + npm_install_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_install_package_instance.yaml.erb')).result(binding) + commit.project = another_project commit.commit_message = 'Add .gitlab-ci.yml' commit.add_files([ - gitlab_ci_install_yaml + { + file_path: '.gitlab-ci.yml', + content: npm_install_yaml + } ]) end end @@ -217,13 +162,6 @@ module QA Page::Project::Packages::Show.perform do |show| expect(show).to have_package_info(package.name, "1.0.0") - - show.click_delete - end - - Page::Project::Packages::Index.perform do |index| - expect(index).to have_content("Package deleted successfully") - expect(index).not_to have_package(package.name) end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb index e25a742493b..cad1802f3e9 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/npm/npm_project_level_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Package Registry', :orchestrated, :packages, :object_storage do + RSpec.describe 'Package Registry', :orchestrated, :reliable, :packages, :object_storage do describe 'npm project level endpoint' do using RSpec::Parameterized::TableSyntax include Runtime::Fixtures @@ -46,62 +46,6 @@ module QA end end - let(:gitlab_ci_yaml) do - { - file_path: '.gitlab-ci.yml', - content: - <<~YAML - image: node:latest - - stages: - - deploy - - install - - deploy: - stage: deploy - script: - - echo "//${CI_SERVER_HOST}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/:_authToken=#{auth_token}">.npmrc - - npm publish - only: - - "#{project.default_branch}" - tags: - - "runner-for-#{project.name}" - install: - stage: install - script: - - "npm config set @#{registry_scope}:registry #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/npm/" - - "npm install #{package.name}" - cache: - key: ${CI_BUILD_REF_NAME} - paths: - - node_modules/ - artifacts: - paths: - - node_modules/ - only: - - "#{project.default_branch}" - tags: - - "runner-for-#{project.name}" - YAML - } - end - - let(:package_json) do - { - file_path: 'package.json', - content: <<~JSON - { - "name": "#{package.name}", - "version": "1.0.0", - "description": "Example package for GitLab npm registry", - "publishConfig": { - "@#{registry_scope}:registry": "#{gitlab_address_with_port}/api/v4/projects/#{project.id}/packages/npm/" - } - } - JSON - } - end - let(:package) do Resource::Package.init do |package| package.name = "@#{registry_scope}/mypackage-#{SecureRandom.hex(8)}" @@ -115,10 +59,10 @@ module QA project.remove_via_api! end - where(:authentication_token_type, :token_name) do - :personal_access_token | 'Personal Access Token' - :ci_job_token | 'CI Job Token' - :project_deploy_token | 'Deploy Token' + where(:case_name, :authentication_token_type, :token_name, :testcase) do + 'using personal access token' | :personal_access_token | 'Personal Access Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347592' + 'using ci job token' | :ci_job_token | 'CI Job Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347594' + 'using project deploy token' | :project_deploy_token | 'Deploy Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347593' end with_them do @@ -133,13 +77,22 @@ module QA end end - it "push and pull a npm package via CI using a #{params[:token_name]}" do + it 'push and pull a npm package via CI', testcase: params[:testcase] do Resource::Repository::Commit.fabricate_via_api! do |commit| + npm_upload_install_yaml = ERB.new(read_fixture('package_managers/npm', 'npm_upload_install_package_project.yaml.erb')).result(binding) + package_json = ERB.new(read_fixture('package_managers/npm', 'package_project.json.erb')).result(binding) + commit.project = project commit.commit_message = 'Add .gitlab-ci.yml' commit.add_files([ - gitlab_ci_yaml, - package_json + { + file_path: '.gitlab-ci.yml', + content: npm_upload_install_yaml + }, + { + file_path: 'package.json', + content: package_json + } ]) end @@ -182,13 +135,6 @@ module QA Page::Project::Packages::Show.perform do |show| expect(show).to have_package_info(package.name, "1.0.0") - - show.click_delete - end - - Page::Project::Packages::Index.perform do |index| - expect(index).to have_content("Package deleted successfully") - expect(index).not_to have_package(package.name) end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb index d63bf486f11..24f83bc19fb 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/nuget_repository_spec.rb @@ -5,6 +5,7 @@ module QA describe 'NuGet Repository' do using RSpec::Parameterized::TableSyntax include Runtime::Fixtures + let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'nuget-package-project' @@ -53,7 +54,7 @@ module QA runner.name = "qa-runner-#{Time.now.to_i}" runner.tags = ["runner-for-#{project.group.name}"] runner.executor = :docker - runner.token = project.group.runners_token + runner.token = project.group.reload!.runners_token end end @@ -62,10 +63,10 @@ module QA package.remove_via_api! end - where(:authentication_token_type, :token_name) do - :personal_access_token | 'Personal Access Token' - :ci_job_token | 'CI Job Token' - :group_deploy_token | 'Deploy Token' + where(:case_name, :authentication_token_type, :token_name, :testcase) do + 'using personal access token' | :personal_access_token | 'Personal Access Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347597' + 'using ci job token' | :ci_job_token | 'CI Job Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347595' + 'using group deploy token' | :group_deploy_token | 'Deploy Token' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347596' end with_them do @@ -91,36 +92,19 @@ module QA end end - it "publishes a nuget package at the project level, installs and deletes it at the group level using a #{params[:token_name]}" do + it 'publishes a nuget package at the project endpoint and installs it from the group endpoint', testcase: params[:testcase] do Flow::Login.sign_in Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + nuget_upload_yaml = ERB.new(read_fixture('package_managers/nuget', 'nuget_upload_package.yaml.erb')).result(binding) commit.project = project commit.commit_message = 'Add .gitlab-ci.yml' commit.update_files( [ { file_path: '.gitlab-ci.yml', - content: <<~YAML - image: mcr.microsoft.com/dotnet/sdk:5.0 - - stages: - - deploy - - deploy: - stage: deploy - script: - - dotnet restore -p:Configuration=Release - - dotnet build -c Release - - dotnet pack -c Release -p:PackageID=#{package.name} - - dotnet nuget add source "$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/packages/nuget/index.json" --name gitlab --username #{auth_token_username} --password #{auth_token_password} --store-password-in-clear-text - - dotnet nuget push "bin/Release/*.nupkg" --source gitlab - rules: - - if: '$CI_COMMIT_BRANCH == "#{project.default_branch}"' - tags: - - "runner-for-#{project.group.name}" - YAML + content: nuget_upload_yaml } ] ) @@ -142,6 +126,8 @@ module QA Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + nuget_install_yaml = ERB.new(read_fixture('package_managers/nuget', 'nuget_install_package.yaml.erb')).result(binding) + commit.project = another_project commit.commit_message = 'Add new csproj file' commit.add_files( @@ -165,23 +151,7 @@ module QA [ { file_path: '.gitlab-ci.yml', - content: <<~YAML - image: mcr.microsoft.com/dotnet/sdk:5.0 - - stages: - - install - - install: - stage: install - script: - - dotnet nuget locals all --clear - - dotnet nuget add source "$CI_SERVER_URL/api/v4/groups/#{another_project.group.id}/-/packages/nuget/index.json" --name gitlab --username #{auth_token_username} --password #{auth_token_password} --store-password-in-clear-text - - "dotnet add otherdotnet.csproj package #{package.name} --version 1.0.0" - only: - - "#{another_project.default_branch}" - tags: - - "runner-for-#{project.group.name}" - YAML + content: nuget_install_yaml } ] ) @@ -204,14 +174,6 @@ module QA Page::Project::Packages::Index.perform do |index| expect(index).to have_package(package.name) - index.click_package(package.name) - end - - Page::Project::Packages::Show.perform(&:click_delete) - - Page::Project::Packages::Index.perform do |index| - expect(index).to have_content('Package deleted successfully') - expect(index).not_to have_package(package.name) end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb index 2e7bd8fc5d7..a0c2eca5bd2 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/pypi_repository_spec.rb @@ -4,6 +4,7 @@ module QA RSpec.describe 'Package', :orchestrated, :packages, :object_storage do describe 'PyPI Repository' do include Runtime::Fixtures + let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'pypi-package-project' @@ -36,56 +37,18 @@ module QA Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + pypi_yaml = ERB.new(read_fixture('package_managers/pypi', 'pypi_upload_install_package.yaml.erb')).result(binding) + pypi_setup_file = ERB.new(read_fixture('package_managers/pypi', 'setup.py.erb')).result(binding) + commit.project = project - commit.commit_message = 'Add .gitlab-ci.yml' + commit.commit_message = 'Add files' commit.add_files([{ file_path: '.gitlab-ci.yml', - content: - <<~YAML - image: python:latest - stages: - - run - - install - - run: - stage: run - script: - - pip install twine - - python setup.py sdist bdist_wheel - - "TWINE_PASSWORD=${CI_JOB_TOKEN} TWINE_USERNAME=gitlab-ci-token python -m twine upload --repository-url #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi dist/*" - tags: - - "runner-for-#{project.name}" - install: - stage: install - script: - - "pip install #{package.name} --no-deps --index-url #{uri.scheme}://#{personal_access_token}:#{personal_access_token}@#{gitlab_host_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/pypi/simple --trusted-host #{gitlab_host_with_port}" - tags: - - "runner-for-#{project.name}" - - YAML + content: pypi_yaml }, { file_path: 'setup.py', - content: - <<~EOF - import setuptools - - setuptools.setup( - name="#{package.name}", - version="0.0.1", - author="Example Author", - author_email="author@example.com", - description="A small example package", - packages=setuptools.find_packages(), - classifiers=[ - "Programming Language :: Python :: 3", - "License :: OSI Approved :: MIT License", - "Operating System :: OS Independent", - ], - python_requires='>=3.6', - ) - EOF - + content: pypi_setup_file }]) end end @@ -119,21 +82,11 @@ module QA end context 'when at the project level' do - it 'publishes and installs a pypi package and deletes it', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348015' do + it 'publishes and installs a pypi package', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348015' do Page::Project::Menu.perform(&:click_packages_link) Page::Project::Packages::Index.perform do |index| expect(index).to have_package(package.name) - index.click_package(package.name) - end - - Page::Project::Packages::Show.perform(&:click_delete) - - Page::Project::Packages::Index.perform do |index| - aggregate_failures do - expect(index).to have_content("Package deleted successfully") - expect(index).not_to have_package(package.name) - end end end end diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb index 062d2b49deb..b2208dc644c 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/rubygems_registry_spec.rb @@ -43,35 +43,21 @@ module QA project.remove_via_api! end - it 'publishes and deletes a Ruby gem', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347649' do + it 'publishes a Ruby gem', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347649' do Flow::Login.sign_in Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| + rubygem_upload_yaml = ERB.new(read_fixture('package_managers/rubygems', 'rubygems_upload_package.yaml.erb')).result(binding) + rubygem_package_gemspec = ERB.new(read_fixture('package_managers/rubygems', 'package.gemspec.erb')).result(binding) + commit.project = project commit.commit_message = 'Add package files' commit.add_files( [ { file_path: '.gitlab-ci.yml', - content: - <<~YAML - image: ruby - - test_package: - stage: deploy - before_script: - - mkdir ~/.gem - - echo "---" > ~/.gem/credentials - - | - echo "#{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/rubygems: '${CI_JOB_TOKEN}'" >> ~/.gem/credentials - - chmod 0600 ~/.gem/credentials - script: - - gem build #{package.name} - - gem push #{package.name}-0.0.1.gem --host #{gitlab_address_with_port}/api/v4/projects/${CI_PROJECT_ID}/packages/rubygems - tags: - - "runner-for-#{project.name}" - YAML + content: rubygem_upload_yaml }, { file_path: 'lib/hello_gem.rb', @@ -86,49 +72,7 @@ module QA }, { file_path: "#{package.name}.gemspec", - content: - <<~RUBY - # frozen_string_literal: true - - Gem::Specification.new do |s| - s.name = '#{package.name}' - s.authors = ['Tanuki Steve', 'Hal 9000'] - s.author = 'Tanuki Steve' - s.version = '0.0.1' - s.date = '2011-09-29' - s.summary = 'this is a test package' - s.files = ['lib/hello_gem.rb'] - s.require_paths = ['lib'] - - s.description = 'A test package for GitLab.' - s.email = 'tanuki@not_real.com' - s.homepage = 'https://gitlab.com/ruby-co/my-package' - s.license = 'MIT' - - s.metadata = { - 'bug_tracker_uri' => 'https://gitlab.com/ruby-co/my-package/issues', - 'changelog_uri' => 'https://gitlab.com/ruby-co/my-package/CHANGELOG.md', - 'documentation_uri' => 'https://gitlab.com/ruby-co/my-package/docs', - 'mailing_list_uri' => 'https://gitlab.com/ruby-co/my-package/mailme', - 'source_code_uri' => 'https://gitlab.com/ruby-co/my-package' - } - - s.bindir = 'bin' - s.platform = Gem::Platform::RUBY - s.post_install_message = 'Installed, thank you!' - s.rdoc_options = ['--main'] - s.required_ruby_version = '>= 2.7.0' - s.required_rubygems_version = '>= 1.8.11' - s.requirements = 'A high powered server or calculator' - s.rubygems_version = '1.8.09' - - s.add_dependency 'dependency_1', '~> 1.2.3' - s.add_dependency 'dependency_2', '3.0.0' - s.add_dependency 'dependency_3', '>= 1.0.0' - s.add_dependency 'dependency_4' - end - - RUBY + content: rubygem_package_gemspec } ] ) @@ -150,14 +94,6 @@ module QA Page::Project::Packages::Index.perform do |index| expect(index).to have_package(package.name) - index.click_package(package.name) - end - - Page::Project::Packages::Show.perform(&:click_delete) - - Page::Project::Packages::Index.perform do |index| - expect(index).to have_content("Package deleted successfully") - expect(index).not_to have_package(package.name) end end 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 f4a5c715684..c86f75e0b16 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 @@ -33,13 +33,13 @@ module QA end keys = [ - [Runtime::Key::RSA, 8192], - [Runtime::Key::ECDSA, 521], - [Runtime::Key::ED25519] + ['https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348022', Runtime::Key::RSA, 8192], + ['https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348021', Runtime::Key::ECDSA, 521], + ['https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348020', Runtime::Key::ED25519] ] - keys.each do |(key_class, bits)| - it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines" do + keys.each do |(testcase, key_class, bits)| + it "user sets up a deploy key with #{key_class}(#{bits}) to clone code using pipelines", testcase: testcase do key = key_class.new(*bits) Resource::DeployKey.fabricate_via_browser_ui! do |resource| diff --git a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb index 2538f249010..718dc9860fb 100644 --- a/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb +++ b/qa/qa/specs/features/browser_ui/7_configure/auto_devops/auto_devops_templates_spec.rb @@ -3,6 +3,8 @@ module QA RSpec.describe 'Configure' do describe 'AutoDevOps Templates', only: { subdomain: :staging } do + using RSpec::Parameterized::TableSyntax + # specify jobs to be disabled in the pipeline. # CANARY_ENABLED will allow the pipeline to be # blocked by a manual job, rather than fail @@ -17,8 +19,8 @@ module QA ] end - where(:template) do - %w[express] + where(:case_name, :template, :testcase) do + 'using express template' | 'express' | 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348075' end with_them do @@ -45,7 +47,7 @@ module QA Flow::Login.sign_in end - it 'works with Auto DevOps' do + it 'works with Auto DevOps', testcase: params[:testcase] do %w[build code_quality test].each do |job| pipeline.visit! diff --git a/qa/qa/specs/helpers/quarantine.rb b/qa/qa/specs/helpers/quarantine.rb index 738c99efb28..e58d70fdfb5 100644 --- a/qa/qa/specs/helpers/quarantine.rb +++ b/qa/qa/specs/helpers/quarantine.rb @@ -10,8 +10,10 @@ module QA extend self - # Skip tests in quarantine unless we explicitly focus on them. + # Skip tests in quarantine unless we explicitly focus on them or quarantine disabled def skip_or_run_quarantined_tests_or_contexts(example) + return if Runtime::Env.quarantine_disabled? + if filters.key?(:quarantine) included_filters = filters_other_than_quarantine diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb index 2c9e302fc56..a861c13a44c 100644 --- a/qa/qa/specs/runner.rb +++ b/qa/qa/specs/runner.rb @@ -7,6 +7,7 @@ module QA module Specs class Runner < Scenario::Template attr_accessor :tty, :tags, :options + RegexMismatchError = Class.new(StandardError) DEFAULT_TEST_PATH_ARGS = ['--', File.expand_path('./features', __dir__)].freeze DEFAULT_STD_ARGS = [$stderr, $stdout].freeze @@ -72,16 +73,48 @@ module QA elsif Runtime::Scenario.attributes[:count_examples_only] args.unshift('--dry-run') out = StringIO.new + RSpec::Core::Runner.run(args.flatten, $stderr, out).tap do |status| abort if status.nonzero? end - $stdout.puts out.string.match(/(\d+) examples,/)[1] + + begin + total_examples = out.string.match(/(\d+) examples?,/)[1] + rescue StandardError + raise RegexMismatchError, 'Rspec output did not match regex' + end + + filename = build_filename + + File.open(filename, 'w') { |f| f.write(total_examples) } if total_examples.to_i > 0 + + $stdout.puts "Total examples in #{Runtime::Scenario.klass}: #{total_examples}#{total_examples.to_i > 0 ? ". Saved to file: #{filename}" : ''}" else RSpec::Core::Runner.run(args.flatten, *DEFAULT_STD_ARGS).tap do |status| abort if status.nonzero? end end end + + private + + def build_filename + filename = Runtime::Scenario.klass.split('::').last(3).join('_').downcase + + tags = [] + options.reduce do |before, after| + tags << after if %w[--tag -t].include?(before) + after + end + tags = tags.compact.join('_') + + filename.concat("_#{tags}") unless tags.empty? + + filename.concat('.txt') + + FileUtils.mkdir_p('no_of_examples') + File.join('no_of_examples', filename) + end end end end diff --git a/qa/qa/support/api.rb b/qa/qa/support/api.rb index 663761805ee..976188e45c6 100644 --- a/qa/qa/support/api.rb +++ b/qa/qa/support/api.rb @@ -44,6 +44,18 @@ module QA end end + def patch(url, payload = nil) + with_retry_on_too_many_requests do + RestClient::Request.execute( + method: :patch, + url: url, + payload: payload, + verify_ssl: false) + rescue RestClient::ExceptionWithResponse => e + return_response_or_raise(e) + end + end + def put(url, payload = nil) with_retry_on_too_many_requests do RestClient::Request.execute( diff --git a/qa/qa/support/formatters/allure_metadata_formatter.rb b/qa/qa/support/formatters/allure_metadata_formatter.rb index 10769ba5c57..da35ffde1ae 100644 --- a/qa/qa/support/formatters/allure_metadata_formatter.rb +++ b/qa/qa/support/formatters/allure_metadata_formatter.rb @@ -15,14 +15,42 @@ module QA def example_started(example_notification) example = example_notification.example - quarantine_issue = example.metadata.dig(:quarantine, :issue) - example.issue('Quarantine issue', quarantine_issue) if quarantine_issue + add_quarantine_issue_link(example) + add_failure_issues_link(example) + add_ci_job_link(example) + end + + private + + # Add quarantine issue links + # + # @param [RSpec::Core::Example] example + # @return [void] + def add_quarantine_issue_link(example) + issue_link = example.metadata.dig(:quarantine, :issue) + + return unless issue_link + return example.issue('Quarantine issue', issue_link) if issue_link.is_a?(String) + return issue_link.each { |link| example.issue('Quarantine issue', link) } if issue_link.is_a?(Array) + end + # Add failure issues link + # + # @param [RSpec::Core::Example] example + # @return [void] + def add_failure_issues_link(example) spec_file = example.file_path.split('/').last example.issue( 'Failure issues', "https://gitlab.com/gitlab-org/gitlab/-/issues?scope=all&state=opened&search=#{spec_file}" ) + end + + # Add ci job link + # + # @param [RSpec::Core::Example] example + # @return [void] + def add_ci_job_link(example) return unless Runtime::Env.running_in_ci? example.add_link(name: "Job(#{Runtime::Env.ci_job_name})", url: Runtime::Env.ci_job_url) diff --git a/qa/qa/support/formatters/test_stats_formatter.rb b/qa/qa/support/formatters/test_stats_formatter.rb index 7678cb8406c..430294b0bb6 100644 --- a/qa/qa/support/formatters/test_stats_formatter.rb +++ b/qa/qa/support/formatters/test_stats_formatter.rb @@ -14,40 +14,38 @@ module QA return log(:warn, 'Missing QA_INFLUXDB_URL, skipping metrics export!') unless influxdb_url return log(:warn, 'Missing QA_INFLUXDB_TOKEN, skipping metrics export!') unless influxdb_token - data = notification.examples.map { |example| test_stats(example) }.compact - influx_client.create_write_api.write(data: data) - log(:info, "Pushed #{data.length} entries to influxdb") - rescue StandardError => e - log(:error, "Failed to push data to influxdb, error: #{e}") + push_test_stats(notification.examples) + push_fabrication_stats end private - # InfluxDb client + # Push test execution stats to influxdb # - # @return [InfluxDB2::Client] - def influx_client - @influx_client ||= InfluxDB2::Client.new( - influxdb_url, - influxdb_token, - bucket: 'e2e-test-stats', - org: 'gitlab-qa', - precision: InfluxDB2::WritePrecision::NANOSECOND - ) - end + # @param [Array<RSpec::Core::Example>] examples + # @return [void] + def push_test_stats(examples) + data = examples.map { |example| test_stats(example) }.compact - # InfluxDb instance url - # - # @return [String] - def influxdb_url - @influxdb_url ||= env('QA_INFLUXDB_URL') + influx_client.write(data: data) + log(:debug, "Pushed #{data.length} test execution entries to influxdb") + rescue StandardError => e + log(:error, "Failed to push test execution stats to influxdb, error: #{e}") end - # Influxdb token + # Push resource fabrication stats to influxdb # - # @return [String] - def influxdb_token - @influxdb_token ||= env('QA_INFLUXDB_TOKEN') + # @return [void] + def push_fabrication_stats + data = Tools::TestResourceDataProcessor.resources.flat_map do |resource, values| + values.map { |v| fabrication_stats(resource: resource, **v) } + end + return if data.empty? + + influx_client.write(data: data) + log(:debug, "Pushed #{data.length} resource fabrication entries to influxdb") + rescue StandardError => e + log(:error, "Failed to push fabrication stats to influxdb, error: #{e}") end # Transform example to influxdb compatible metrics data @@ -73,7 +71,8 @@ module QA job_name: job_name, merge_request: merge_request, run_type: env('QA_RUN_TYPE') || run_type, - stage: devops_stage(file_path) + stage: devops_stage(file_path), + testcase: example.metadata[:testcase] }, fields: { id: example.id, @@ -84,8 +83,7 @@ module QA retry_attempts: example.metadata[:retry_attempts] || 0, job_url: QA::Runtime::Env.ci_job_url, pipeline_url: env('CI_PIPELINE_URL'), - pipeline_id: env('CI_PIPELINE_ID'), - testcase: example.metadata[:testcase] + pipeline_id: env('CI_PIPELINE_ID') } } rescue StandardError => e @@ -93,6 +91,34 @@ module QA nil end + # Resource fabrication data point + # + # @param [String] resource + # @param [String] info + # @param [Symbol] fabrication_method + # @param [Symbol] http_method + # @param [Integer] fabrication_time + # @return [Hash] + def fabrication_stats(resource:, info:, fabrication_method:, http_method:, fabrication_time:, timestamp:, **) + { + name: 'fabrication-stats', + time: time, + tags: { + resource: resource, + fabrication_method: fabrication_method, + http_method: http_method, + run_type: env('QA_RUN_TYPE') || run_type, + merge_request: merge_request + }, + fields: { + fabrication_time: fabrication_time, + info: info, + job_url: QA::Runtime::Env.ci_job_url, + timestamp: timestamp + } + } + end + # Project name # # @return [String] @@ -150,7 +176,7 @@ module QA # @param [String] message # @return [void] def log(level, message) - QA::Runtime::Logger.public_send(level, "influxdb exporter: #{message}") + QA::Runtime::Logger.public_send(level, "[influxdb exporter]: #{message}") end # Return non empty environment variable value @@ -170,6 +196,33 @@ module QA def devops_stage(file_path) file_path.match(%r{\d{1,2}_(\w+)/})&.captures&.first end + + # InfluxDb client + # + # @return [InfluxDB2::WriteApi] + def influx_client + @influx_client ||= InfluxDB2::Client.new( + influxdb_url, + influxdb_token, + bucket: 'e2e-test-stats', + org: 'gitlab-qa', + precision: InfluxDB2::WritePrecision::NANOSECOND + ).create_write_api + end + + # InfluxDb instance url + # + # @return [String] + def influxdb_url + @influxdb_url ||= env('QA_INFLUXDB_URL') + end + + # Influxdb token + # + # @return [String] + def influxdb_token + @influxdb_token ||= env('QA_INFLUXDB_TOKEN') + end end end end diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb index f5299ed840d..b402639b843 100644 --- a/qa/qa/support/page/logging.rb +++ b/qa/qa/support/page/logging.rb @@ -4,6 +4,8 @@ module QA module Support module Page module Logging + using Rainbow + def assert_no_element(name) log("asserting no element :#{name}") @@ -17,7 +19,7 @@ module QA end def scroll_to(selector, text: nil) - msg = "scrolling to :#{selector}" + msg = "scrolling to :#{Rainbow(selector).underline.bright}" msg += " with text: #{text}" if text log(msg) @@ -37,7 +39,7 @@ module QA element = super - log("found :#{name}") if element + log("found :#{Rainbow(name).underline.bright}") element end @@ -47,7 +49,7 @@ module QA elements = super - log("found #{elements.size} :#{name}") if elements + log("found #{elements.size} :#{Rainbow(name).underline.bright}") if elements elements end @@ -71,7 +73,7 @@ module QA end def click_element(name, page = nil, **kwargs) - msg = ["clicking :#{name}"] + msg = ["clicking :#{Rainbow(name).underline.bright}"] msg << ", expecting to be at #{page.class}" if page msg << "with args #{kwargs}" @@ -170,7 +172,7 @@ module QA end def log_has_element_or_not(method, name, found, **kwargs) - msg = ["#{method} :#{name}"] + msg = ["#{method} :#{Rainbow(name).underline.bright}"] msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text] msg << "class: #{kwargs[:class]}" if kwargs[:class] msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})" diff --git a/qa/qa/support/repeater.rb b/qa/qa/support/repeater.rb index a4e8035f964..1b9aa809051 100644 --- a/qa/qa/support/repeater.rb +++ b/qa/qa/support/repeater.rb @@ -1,10 +1,11 @@ # frozen_string_literal: true - require 'active_support/inflector' +require 'rainbow/refinement' module QA module Support module Repeater + using Rainbow DEFAULT_MAX_WAIT_TIME = 60 RepeaterConditionExceededError = Class.new(RuntimeError) @@ -39,7 +40,7 @@ module QA QA::Runtime::Logger.debug(msg.join(' ')) end - QA::Runtime::Logger.debug("Attempt number #{attempts + 1}") if log && max_attempts && attempts > 0 + QA::Runtime::Logger.debug("Attempt number #{attempts + 1}".bg(:yellow).black) if log && max_attempts && attempts > 0 result = yield if result diff --git a/qa/qa/tools/delete_test_resources.rb b/qa/qa/tools/delete_test_resources.rb index 917cb2fa992..6f63c56f736 100644 --- a/qa/qa/tools/delete_test_resources.rb +++ b/qa/qa/tools/delete_test_resources.rb @@ -1,8 +1,8 @@ # frozen_string_literal: true -# This script reads from test_resources.txt file to collect data about resources to delete -# Deletes all deletable resources that E2E tests created -# Resource type: Sandbox, User, Fork and RSpec::Mocks::Double are not included +# This script reads from test-resources JSON file to collect data about resources to delete +# Filter out resources that cannot be deleted +# Then deletes all deletable resources that E2E tests created # # Required environment variables: GITLAB_QA_ACCESS_TOKEN and GITLAB_ADDRESS # When in CI also requires: QA_TEST_RESOURCES_FILE_PATTERN @@ -13,7 +13,15 @@ module QA class DeleteTestResources include Support::API - def initialize(file_pattern = nil) + IGNORED_RESOURCES = [ + 'QA::Resource::PersonalAccessToken', + 'QA::Resource::CiVariable', + 'QA::Resource::Repository::Commit', + 'QA::EE::Resource::GroupIteration', + 'QA::EE::Resource::Settings::Elasticsearch' + ].freeze + + def initialize(file_pattern = Runtime::Env.test_resources_created_filepath) raise ArgumentError, "Please provide GITLAB_ADDRESS" unless ENV['GITLAB_ADDRESS'] raise ArgumentError, "Please provide GITLAB_QA_ACCESS_TOKEN" unless ENV['GITLAB_QA_ACCESS_TOKEN'] @@ -22,45 +30,58 @@ module QA end def run - puts 'Deleting test created resources...' - - if Runtime::Env.running_in_ci? - raise ArgumentError, 'Please provide QA_TEST_RESOURCES_FILE_PATTERN' unless ENV['QA_TEST_RESOURCES_FILE_PATTERN'] - - Dir.glob(@file_pattern).each do |file| - delete_resources(load_file(file)) - end - else - file = Runtime::Env.test_resources_created_filepath - raise ArgumentError, "'#{file}' either does not exist or empty." if !File.exist?(file) || File.zero?(file) - - delete_resources(load_file(file)) + failures = files.flat_map do |file| + resources = read_file(file) + filtered_resources = filter_resources(resources) + delete_resources(filtered_resources) end - puts "\nDone" + return puts "\nDone" if failures.empty? + + puts "\nFailed to delete #{failures.size} resources:\n" + puts failures end private - def load_file(json) - JSON.parse(File.read(json)) + def files + puts "Gathering JSON files...\n" + files = Dir.glob(@file_pattern) + abort("There is no file with this pattern #{@file_pattern}") if files.empty? + + files.reject { |file| File.zero?(file) } + + files end - def delete_resources(resources) - failures = [] + def read_file(file) + JSON.parse(File.read(file)) + end - resources.each_key do |type| - next if resources[type].empty? + def filter_resources(resources) + puts "Filtering resources - Only keep deletable resources...\n" - resources[type].each do |resource| - next if resource_not_found?(resource['api_path']) + transformed_values = resources.transform_values! do |v| + v.reject do |attributes| + attributes['info'] == "with full_path 'gitlab-qa-sandbox-group'" || + attributes['http_method'] == 'get' && !attributes['info']&.include?("with username 'qa-") || + attributes['api_path'] == 'Cannot find resource API path' + end + end + + transformed_values.reject! { |k, v| v.empty? || IGNORED_RESOURCES.include?(k) } + end - msg = resource['info'] ? "#{type} - #{resource['info']}" : "#{type} at #{resource['api_path']}" + def delete_resources(resources) + resources.each_with_object([]) do |(key, value), failures| + value.each do |resource| + next if resource_not_found?(resource['api_path']) + msg = resource['info'] ? "#{key} - #{resource['info']}" : "#{key} at #{resource['api_path']}" puts "\nDeleting #{msg}..." delete_response = delete(Runtime::API::Request.new(@api_client, resource['api_path']).url) - if delete_response.code == 202 + if delete_response.code == 202 || delete_response.code == 204 print "\e[32m.\e[0m" else print "\e[31mF\e[0m" @@ -68,17 +89,11 @@ module QA end end end - - unless failures.empty? - puts "\nFailed to delete #{failures.length} resources:\n" - puts failures - end end def resource_not_found?(api_path) - get_response = get Runtime::API::Request.new(@api_client, api_path).url - - get_response.code.eql? 404 + # if api path contains param "?hard_delete=<boolean>", remove it + get(Runtime::API::Request.new(@api_client, api_path.split('?').first).url).code.eql? 404 end end end diff --git a/qa/qa/tools/initialize_gitlab_auth.rb b/qa/qa/tools/initialize_gitlab_auth.rb index 86791f1f624..18e90f0d739 100644 --- a/qa/qa/tools/initialize_gitlab_auth.rb +++ b/qa/qa/tools/initialize_gitlab_auth.rb @@ -16,7 +16,7 @@ module QA def run Runtime::Scenario.define(:gitlab_address, address) - puts "Signing in and creating the default password for the root user if it's not set already..." + QA::Runtime::Logger.info("Signing in and creating the default password for the root user if it's not set already...") QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login) Flow::Login.sign_in diff --git a/qa/qa/tools/reliable_report.rb b/qa/qa/tools/reliable_report.rb index b99b97c1ea6..27e54c2d8bf 100644 --- a/qa/qa/tools/reliable_report.rb +++ b/qa/qa/tools/reliable_report.rb @@ -58,7 +58,7 @@ module QA { title: "Reliable e2e test report", description: report_issue_body, - labels: "Quality,test,type::maintenance,reliable test report" + labels: "Quality,test,type::maintenance,reliable test report,automation:devops-mapping-disable" }, headers: { "PRIVATE-TOKEN" => gitlab_access_token } ) diff --git a/qa/qa/tools/test_resource_data_processor.rb b/qa/qa/tools/test_resource_data_processor.rb index 78fb6ef6cd0..965919dc516 100644 --- a/qa/qa/tools/test_resource_data_processor.rb +++ b/qa/qa/tools/test_resource_data_processor.rb @@ -6,60 +6,81 @@ module QA module Tools class TestResourceDataProcessor - @resources ||= Hash.new { |hsh, key| hsh[key] = [] } + include Singleton + + def initialize + @resources = Hash.new { |hsh, key| hsh[key] = [] } + end class << self - # Ignoring rspec-mocks, sandbox, user and fork resources - # TODO: Will need to figure out which user resources can be collected, ignore for now - # - # Collecting resources created in E2E tests - # Data is a Hash of resources with keys as resource type (group, project, issue, etc.) - # Each type contains an array of resource object (hash) of the same type - # E.g: { "QA::Resource::Project": [ { info: 'foo', api_path: '/foo'}, {...} ] } - def collect(resource, info) - return if resource.api_response.nil? || - resource.is_a?(RSpec::Mocks::Double) || - resource.is_a?(Resource::Sandbox) || - resource.is_a?(Resource::User) || - resource.is_a?(Resource::Fork) + delegate :collect, :write_to_file, :resources, to: :instance + end - api_path = if resource.respond_to?(:api_delete_path) - resource.api_delete_path.gsub('%2F', '/') - elsif resource.respond_to?(:api_get_path) - resource.api_get_path.gsub('%2F', '/') - else - 'Cannot find resource API path' - end + # @return [Hash<String, Array>] + attr_reader :resources - type = resource.class.name + # Collecting resources created in E2E tests + # Data is a Hash of resources with keys as resource type (group, project, issue, etc.) + # Each type contains an array of resource object (hash) of the same type + # E.g: { "QA::Resource::Project": [ { info: 'foo', api_path: '/foo'}, {...} ] } + # + # @param [QA::Resource::Base] resource fabricated resource + # @param [String] info resource info + # @param [Symbol] method fabrication method, api or browser_ui + # @param [Integer] time fabrication time + # @return [Hash] + def collect(resource:, info:, fabrication_method:, fabrication_time:) + api_path = resource_api_path(resource) + type = resource.class.name - @resources[type] << { info: info, api_path: api_path } - end + resources[type] << { + info: info, + api_path: api_path, + fabrication_method: fabrication_method, + fabrication_time: fabrication_time, + http_method: resource.api_fabrication_http_method, + timestamp: Time.now.to_s + } + end + + # If JSON file exists and not empty, read and load file content + # Merge what is saved in @resources into the content from file + # Overwrite file content with the new data hash + # Otherwise create file and write data hash to file for the first time + # + # @return [void] + def write_to_file + return if resources.empty? - # If JSON file exists and not empty, read and load file content - # Merge what is saved in @resources into the content from file - # Overwrite file content with the new data hash - # Otherwise create file and write data hash to file for the first time - def write_to_file - return if @resources.empty? + file = Pathname.new(Runtime::Env.test_resources_created_filepath) + FileUtils.mkdir_p(file.dirname) - file = Runtime::Env.test_resources_created_filepath - FileUtils.mkdir_p('tmp/') - FileUtils.touch(file) - data = nil + data = resources.deep_dup + # merge existing json if present + JSON.parse(File.read(file)).deep_merge!(data) { |key, val, other_val| val + other_val } if file.exist? + + File.write(file, JSON.pretty_generate(data)) + end - if File.zero?(file) - data = @resources - else - data = JSON.parse(File.read(file)) + private - @resources.each_pair do |key, val| - data[key].nil? ? data[key] = val : val.each { |item| data[key] << item } - end - end + # Determine resource api path or return default value + # Some resources fabricated via UI can raise no attribute error + # + # @param [QA::Resource::Base] resource + # @return [String] + def resource_api_path(resource) + default = 'Cannot find resource API path' - File.open(file, 'w') { |f| f.write(JSON.pretty_generate(data.each_value(&:uniq!))) } + if resource.respond_to?(:api_delete_path) + resource.api_delete_path.gsub('%2F', '/') + elsif resource.respond_to?(:api_get_path) + resource.api_get_path.gsub('%2F', '/') + else + default end + rescue QA::Resource::Base::NoValueError + default end end end diff --git a/qa/qa/vendor/smocker/event_payload.rb b/qa/qa/vendor/smocker/event_payload.rb new file mode 100644 index 00000000000..4bf154b76c2 --- /dev/null +++ b/qa/qa/vendor/smocker/event_payload.rb @@ -0,0 +1,49 @@ +# frozen_string_literal: true + +module QA + module Vendor + module Smocker + class EventPayload + def initialize(hook_data) + @hook_data = hook_data + end + + def raw + @hook_data + end + + def event + raw[:object_kind]&.to_sym + end + + def project_name + raw.dig(:project, :name) + end + + def mr? + event == :merge_request + end + + def issue? + event == :issue + end + + def note? + event == :note + end + + def push? + event == :push + end + + def tag? + event == :tag + end + + def wiki? + event == :wiki_page + end + end + end + end +end diff --git a/qa/qa/vendor/smocker/history_response.rb b/qa/qa/vendor/smocker/history_response.rb new file mode 100644 index 00000000000..53d5759ef8b --- /dev/null +++ b/qa/qa/vendor/smocker/history_response.rb @@ -0,0 +1,52 @@ +# frozen_string_literal: true + +require_relative './event_payload' +require 'time' + +module QA + module Vendor + module Smocker + class HistoryResponse + attr_reader :payload + + def initialize(payload) + @payload = payload + end + + # Smocker context including call counter + def context + payload[:context] + end + + # Smocker request data + def request + payload[:request] + end + + # @return [EventPayload] the request body as a webhook event + def as_hook_event + body = request&.dig(:body) + EventPayload.new body if body + end + + # @return [Time] Time request was recieved + def received + date = request&.dig(:date) + Time.parse date if date + end + + # Find time elapsed since <target> + # + # @param target [Time] target time + # @return [Integer] seconds elapsed since <target> + def elapsed(target) + (received.to_f - target.to_f).round if received + end + + def response + payload[:response] + end + end + end + end +end diff --git a/qa/qa/vendor/smocker/smocker_api.rb b/qa/qa/vendor/smocker/smocker_api.rb new file mode 100644 index 00000000000..3f595b58886 --- /dev/null +++ b/qa/qa/vendor/smocker/smocker_api.rb @@ -0,0 +1,140 @@ +# frozen_string_literal: true + +module QA + module Vendor + module Smocker + class SmockerApi + include Scenario::Actable + include Support::API + + DEFAULT_MOCK = <<~YAML + - request: + method: POST + path: /default + response: + headers: + Content-Type: application/json + body: '{}' + YAML + + # @param wait [Integer] seconds to wait for server + # @yieldparam [SmockerApi] the api object ready for interaction + def self.init(**wait_args) + if @container.nil? + @container = Service::DockerRun::Smocker.new + @container.register! + @container.wait_for_running + end + + yield new(@container, **wait_args) + end + + def self.teardown! + @container&.remove! + end + + def initialize(container, **wait_args) + @container = container + wait_for_ready(**wait_args) + end + + # @return [String] Base url of mock endpoint + def base_url + @container.base_url + end + + # @return [String] Url of admin endpoint + def admin_url + @container.admin_url + end + + # @param endpoint [String] path for mock endpoint + # @return [String] url for mock endpoint + def url(endpoint = 'default') + "#{base_url}/#{endpoint}" + end + + # Waits for the smocker server to be ready + # + # @param wait [Integer] wait duration for smocker readiness + def wait_for_ready(wait: 10) + Support::Waiter.wait_until(max_duration: wait, reload_page: false, raise_on_failure: true) do + ready? + end + end + + # Is smocker server ready for interaction? + # + # @return [Boolean] + def ready? + QA::Runtime::Logger.debug 'Checking Smocker readiness' + get("#{admin_url}/version") + true + # rescuing StandardError because RestClient::ExceptionWithResponse isn't propagating + rescue StandardError => e + QA::Runtime::Logger.debug "Smocker not ready yet \n #{e}" + false + end + + # Clears mocks and history + # + # @param force [Boolean] remove locked mocks? + # @return [Boolean] reset was successful? + def reset(force: true) + response = post("#{admin_url}/reset?force=#{force}", {}.to_json) + parse_body(response)['message'] == 'Reset successful' + end + + # Fetches an active session id from a name + # + # @param name [String] the name of the session + # @return [String] the unique session id + def get_session_id(name) + sessions = parse_body get("#{admin_url}/sessions/summary") + current = sessions.find do |session| + session[:name] == name + end + current&.dig(:id) + end + + # Registers a mock to Smocker + # If a session name is provided, the mock will register to that session + # https://smocker.dev/technical-documentation/mock-definition.html + # + # @param yaml [String] the yaml representing the mock + # @param session [String] the session name for the mock + def register(yaml = DEFAULT_MOCK, session: nil) + query_params = build_params(session: session) + url = "#{admin_url}/mocks?#{query_params}" + headers = { 'Content-Type' => 'application/x-yaml' } + response = post(url, yaml, headers: headers) + parse_body(response) + end + + # Fetches call history for a mock + # + # @param session_name [String] the session name for the mock + # @return [Array<HistoryResponse>] + def history(session_name = nil) + query_params = session_name ? build_params(session: get_session_id(session_name)) : '' + response = get("#{admin_url}/history?#{query_params}") + body = parse_body(response) + + raise body[:message] unless body.is_a?(Array) + + body.map do |entry| + HistoryResponse.new(entry) + end + end + + private + + def build_params(**args) + args.each_with_object([]) do |(k, v), memo| + memo << "#{k}=#{v}" if v + end.join("&") + end + end + end + end +end diff --git a/qa/spec/resource/base_spec.rb b/qa/spec/resource/base_spec.rb index 2dd25f983bf..eab205ec5d1 100644 --- a/qa/spec/resource/base_spec.rb +++ b/qa/spec/resource/base_spec.rb @@ -3,10 +3,45 @@ RSpec.describe QA::Resource::Base do include QA::Support::Helpers::StubEnv - let(:resource) { spy('resource', username: 'qa') } + let(:resource) { spy('resource') } let(:location) { 'http://location' } let(:log_regex) { %r{==> Built a MyResource with username 'qa' via #{method} in [\d.\-e]+ seconds+} } + before do + allow(QA::Tools::TestResourceDataProcessor).to receive(:collect) + allow(QA::Tools::TestResourceDataProcessor).to receive(:write_to_file) + end + + shared_context 'with simple resource' do + subject do + Class.new(QA::Resource::Base) do + def self.name + 'MyResource' + end + + attribute :test do + 'block' + end + + attribute :username do + 'qa' + end + + attribute :no_block + + def fabricate!(*args) + 'any' + end + + def self.current_url + 'http://stub' + end + end + end + + let(:resource) { subject.new } + end + shared_context 'with fabrication context' do subject do Class.new(described_class) do @@ -56,23 +91,29 @@ RSpec.describe QA::Resource::Base do end describe '.fabricate_via_api!' do - include_context 'with fabrication context' + context 'when fabricating' do + include_context 'with fabrication context' - it_behaves_like 'fabrication method', :fabricate_via_api! + 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) + 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: []) + result = subject.fabricate_via_api!(resource: resource, parents: []) - expect(result).to eq(resource) + expect(result).to eq(resource) + end end context "with debug log level" do + include_context 'with simple resource' + let(:method) { 'api' } before do allow(QA::Runtime::Logger).to receive(:debug) + allow(resource).to receive(:api_support?).and_return(true) + allow(resource).to receive(:fabricate_via_api!) end it 'logs the resource and build method' do @@ -88,27 +129,32 @@ RSpec.describe QA::Resource::Base do end describe '.fabricate_via_browser_ui!' do - include_context 'with fabrication context' + context 'when fabricating' do + include_context 'with fabrication context' - it_behaves_like 'fabrication method', :fabricate_via_browser_ui!, :fabricate! + 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: []) + 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 + expect(resource).to have_received(:fabricate!).with('something') + end - it 'returns fabrication resource' do - result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) + it 'returns fabrication resource' do + result = subject.fabricate_via_browser_ui!('something', resource: resource, parents: []) - expect(result).to eq(resource) + expect(result).to eq(resource) + end end context "with debug log level" do + include_context 'with simple resource' + let(:method) { 'browser_ui' } before do allow(QA::Runtime::Logger).to receive(:debug) + # allow(resource).to receive(:fabricate!) end it 'logs the resource and build method' do @@ -123,28 +169,6 @@ RSpec.describe QA::Resource::Base do end end - shared_context 'with 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 'with simple resource' diff --git a/qa/spec/resource/reusable_collection_spec.rb b/qa/spec/resource/reusable_collection_spec.rb new file mode 100644 index 00000000000..9116462b396 --- /dev/null +++ b/qa/spec/resource/reusable_collection_spec.rb @@ -0,0 +1,110 @@ +# frozen_string_literal: true + +RSpec.describe QA::Resource::ReusableCollection do + let(:reusable_resource_class) do + Class.new do + prepend QA::Resource::Reusable + + attr_reader :removed + + def self.name + 'FooReusableResource' + end + + def comparable + self.class.name + end + + def remove_via_api! + @removed = true + end + + def exists?() end + end + end + + let(:another_reusable_resource_class) do + Class.new(reusable_resource_class) do + def self.name + 'BarReusableResource' + end + end + end + + let(:a_resource_instance) { reusable_resource_class.new } + let(:another_resource_instance) { another_reusable_resource_class.new } + + it 'is a singleton class' do + expect { described_class.new }.to raise_error(NoMethodError) + end + + subject(:collection) do + described_class.instance + end + + before do + described_class.register_resource_classes do |c| + reusable_resource_class.register(c) + another_reusable_resource_class.register(c) + end + + collection.resource_classes = { + 'FooReusableResource' => { + reuse_as_identifier: { + resource: a_resource_instance + } + }, + 'BarReusableResource' => { + another_reuse_as_identifier: { + resource: another_resource_instance + } + } + } + + allow(a_resource_instance).to receive(:validate_reuse) + allow(another_resource_instance).to receive(:validate_reuse) + end + + after do + collection.resource_classes = {} + end + + describe '#each_resource' do + it 'yields each resource and reuse_as identifier in the collection' do + expect { |blk| collection.each_resource(&blk) } + .to yield_successive_args( + [:reuse_as_identifier, a_resource_instance], + [:another_reuse_as_identifier, another_resource_instance] + ) + end + end + + describe '.remove_all_via_api!' do + before do + allow(a_resource_instance).to receive(:exists?).and_return(true) + allow(another_resource_instance).to receive(:exists?).and_return(true) + end + + it 'removes each instance of each resource class' do + described_class.remove_all_via_api! + + expect(a_resource_instance.removed).to be true + expect(another_resource_instance.removed).to be true + end + end + + describe '.validate_resource_reuse' do + it 'validates each instance of each resource class' do + expect(a_resource_instance).to receive(:validate_reuse) + expect(another_resource_instance).to receive(:validate_reuse) + + described_class.validate_resource_reuse + end + end + + describe '.register_resource_classes' do + it 'yields the hash of resource classes in the collection' do + expect { |blk| described_class.register_resource_classes(&blk) }.to yield_with_args(collection.resource_classes) + end + end +end diff --git a/qa/spec/spec_helper.rb b/qa/spec/spec_helper.rb index 47791f76970..2a6acd6d014 100644 --- a/qa/spec/spec_helper.rb +++ b/qa/spec/spec_helper.rb @@ -6,6 +6,7 @@ require 'securerandom' require 'pathname' require 'active_support/core_ext/hash' require 'active_support/core_ext/object/blank' +require 'rainbow/refinement' require_relative 'qa_deprecation_toolkit_env' QaDeprecationToolkitEnv.configure! @@ -27,8 +28,16 @@ RSpec.configure do |config| config.add_formatter QA::Support::Formatters::QuarantineFormatter config.add_formatter QA::Support::Formatters::TestStatsFormatter if QA::Runtime::Env.export_metrics? + config.before(:suite) do |suite| + QA::Resource::ReusableCollection.register_resource_classes do |collection| + QA::Resource::ReusableProject.register(collection) + QA::Resource::ReusableGroup.register(collection) + end + end + config.prepend_before do |example| QA::Runtime::Logger.debug("\nStarting test: #{example.full_description}\n") + QA::Runtime::Example.current = example # Reset fabrication counters tracked in resource base Thread.current[:api_fabrication] = 0 @@ -65,11 +74,15 @@ RSpec.configure do |config| end config.after(:suite) do |suite| - # If any tests failed, leave the resources behind to help troubleshoot - QA::Resource::ReusableProject.remove_all_via_api! unless suite.reporter.failed_examples.present? - # Write all test created resources to JSON file QA::Tools::TestResourceDataProcessor.write_to_file + + # If requested, confirm that resources were used appropriately (e.g., not left with changes that interfere with + # further reuse) + QA::Resource::ReusableCollection.validate_resource_reuse if QA::Runtime::Env.validate_resource_reuse? + + # If any tests failed, leave the resources behind to help troubleshoot, otherwise remove them. + QA::Resource::ReusableCollection.remove_all_via_api! unless suite.reporter.failed_examples.present? end config.append_after(:suite) do diff --git a/qa/spec/specs/helpers/quarantine_spec.rb b/qa/spec/specs/helpers/quarantine_spec.rb index 8ea375cdb05..0f2d592d771 100644 --- a/qa/spec/specs/helpers/quarantine_spec.rb +++ b/qa/spec/specs/helpers/quarantine_spec.rb @@ -104,6 +104,20 @@ RSpec.describe QA::Specs::Helpers::Quarantine do end describe '.skip_or_run_quarantined_tests_or_contexts' do + context 'with explicitly disabled quarantine' do + before do + stub_env('DISABLE_QUARANTINE', 'true') + end + + it 'runs quarantined test' do + group = describe_successfully do + it('is pending', :quarantine) {} + end + + expect(group.examples.first.execution_result.status).to eq(:passed) + end + end + context 'with no tag focused' do it 'skips quarantined tests' do group = describe_successfully do diff --git a/qa/spec/specs/runner_spec.rb b/qa/spec/specs/runner_spec.rb index 5cc9ff403cd..e52ca1fb17c 100644 --- a/qa/spec/specs/runner_spec.rb +++ b/qa/spec/specs/runner_spec.rb @@ -14,6 +14,7 @@ RSpec.describe QA::Specs::Runner do allow(QA::Runtime::Browser).to receive(:configure!) QA::Runtime::Scenario.define(:gitlab_address, "http://gitlab.test") + QA::Runtime::Scenario.define(:klass, "QA::Scenario::Test::Instance::All") end it_behaves_like 'excludes orchestrated, transient, and geo' @@ -43,6 +44,43 @@ RSpec.describe QA::Specs::Runner do subject.perform end + it 'writes to file when examples are more than zero' do + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect(File).to receive(:open).with('no_of_examples/test_instance_all.txt', 'w') { '22' } + + subject.perform + end + + it 'does not write to file when zero examples' do + out.string = '0 examples,' + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect(File).not_to receive(:open) + + subject.perform + end + + it 'raises error when Rspec output does not match regex' do + out.string = '0' + allow(RSpec::Core::Runner).to receive(:run).and_return(0) + + expect { subject.perform } + .to raise_error(QA::Specs::Runner::RegexMismatchError, 'Rspec output did not match regex') + end + + context 'when --tag is specified as an option' do + subject { described_class.new.tap { |runner| runner.options = %w[--tag actioncable] } } + + it 'includes the option value in the file name' do + expect_rspec_runner_arguments(['--dry-run', '--tag', '~geo', '--tag', 'actioncable', *described_class::DEFAULT_TEST_PATH_ARGS], [$stderr, anything]) + + expect(File).to receive(:open).with('no_of_examples/test_instance_all_actioncable.txt', 'w') { '22' } + + subject.perform + end + end + after do QA::Runtime::Scenario.attributes.delete(:count_examples_only) end diff --git a/qa/spec/support/formatters/test_stats_formatter_spec.rb b/qa/spec/support/formatters/test_stats_formatter_spec.rb index 2bfd7863653..84fc3b83185 100644 --- a/qa/spec/support/formatters/test_stats_formatter_spec.rb +++ b/qa/spec/support/formatters/test_stats_formatter_spec.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true require 'rspec/core/sandbox' +require 'active_support/testing/time_helpers' describe QA::Support::Formatters::TestStatsFormatter do include QA::Support::Helpers::StubEnv include QA::Specs::Helpers::RSpec + include ActiveSupport::Testing::TimeHelpers let(:url) { "http://influxdb.net" } let(:token) { "token" } @@ -22,6 +24,7 @@ describe QA::Support::Formatters::TestStatsFormatter do let(:file_path) { "./qa/specs/features/#{stage}/subfolder/some_spec.rb" } let(:ui_fabrication) { 0 } let(:api_fabrication) { 0 } + let(:fabrication_resources) { {} } let(:influx_client_args) do { @@ -45,7 +48,8 @@ describe QA::Support::Formatters::TestStatsFormatter do job_name: "test-job", merge_request: "false", run_type: run_type, - stage: stage.match(%r{\d{1,2}_(\w+)}).captures.first + stage: stage.match(%r{\d{1,2}_(\w+)}).captures.first, + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' }, fields: { id: './spec/support/formatters/test_stats_formatter_spec.rb[1:1]', @@ -56,8 +60,7 @@ describe QA::Support::Formatters::TestStatsFormatter do retry_attempts: 0, job_url: ci_job_url, pipeline_url: ci_pipeline_url, - pipeline_id: ci_pipeline_id, - testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234' + pipeline_id: ci_pipeline_id } } end @@ -88,6 +91,7 @@ describe QA::Support::Formatters::TestStatsFormatter do before do allow(InfluxDB2::Client).to receive(:new).with(url, token, **influx_client_args) { influx_client } + allow(QA::Tools::TestResourceDataProcessor).to receive(:resources) { fabrication_resources } end context "without influxdb variables configured" do @@ -135,6 +139,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it('spec', :reliable, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {} end + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -147,6 +152,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it('spec', :quarantine, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/1234') {} end + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -162,6 +168,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it 'exports data to influxdb with correct run type' do run_spec + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -179,6 +186,7 @@ describe QA::Support::Formatters::TestStatsFormatter do it 'exports data to influxdb with correct run type' do run_spec + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end @@ -195,8 +203,54 @@ describe QA::Support::Formatters::TestStatsFormatter do it 'exports data to influxdb with fabrication times' do run_spec + expect(influx_write_api).to have_received(:write).once expect(influx_write_api).to have_received(:write).with(data: [data]) end end + + context 'with fabrication resources' do + let(:fabrication_resources) do + { + 'QA::Resource::Project' => [{ + info: "with id '1'", + api_path: '/project', + fabrication_method: :api, + fabrication_time: 1, + http_method: :post, + timestamp: Time.now.to_s + }] + } + end + + let(:fabrication_data) do + { + name: 'fabrication-stats', + time: DateTime.strptime(ci_timestamp).to_time, + tags: { + resource: 'QA::Resource::Project', + fabrication_method: :api, + http_method: :post, + run_type: run_type, + merge_request: "false" + }, + fields: { + fabrication_time: 1, + info: "with id '1'", + job_url: ci_job_url, + timestamp: Time.now.to_s + } + } + end + + around do |example| + freeze_time { example.run } + end + + it 'exports fabrication stats data to influxdb' do + run_spec + + expect(influx_write_api).to have_received(:write).with(data: [fabrication_data]) + end + end end end diff --git a/qa/spec/support/shared_contexts/packages_registry_shared_context.rb b/qa/spec/support/shared_contexts/packages_registry_shared_context.rb index 348176d264b..73a6c2bd99e 100644 --- a/qa/spec/support/shared_contexts/packages_registry_shared_context.rb +++ b/qa/spec/support/shared_contexts/packages_registry_shared_context.rb @@ -32,7 +32,7 @@ module QA runner.name = "qa-runner-#{Time.now.to_i}" runner.tags = ["runner-for-#{package_project.group.name}"] runner.executor = :docker - runner.token = package_project.group.runners_token + runner.token = package_project.group.reload!.runners_token end end diff --git a/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb b/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb index 40a8be8202a..4bbad9bf3e5 100644 --- a/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb +++ b/qa/spec/support/shared_examples/merge_with_code_owner_shared_examples.rb @@ -31,19 +31,13 @@ module QA end # Require approval from code owners on the default branch - # The default branch is already protected, and we can't update a protected branch via the API (yet) - # so we unprotect it first and then protect it again with the desired parameters - Resource::ProtectedBranch.unprotect_via_api! do |protected_branch| - protected_branch.project = project - protected_branch.branch_name = project.default_branch - end - - Resource::ProtectedBranch.fabricate_via_api! do |protected_branch| - protected_branch.project = project - protected_branch.branch_name = project.default_branch - protected_branch.new_branch = false - protected_branch.require_code_owner_approval = true + protected_branch = Resource::ProtectedBranch.fabricate_via_api! do |branch| + branch.project = project + branch.branch_name = project.default_branch + branch.new_branch = false + branch.require_code_owner_approval = true end + protected_branch.set_require_code_owner_approval # Push a change to the file with a CODEOWNERS rule Resource::Repository::Push.fabricate! do |push| diff --git a/qa/spec/tools/reliable_report_spec.rb b/qa/spec/tools/reliable_report_spec.rb index 1ff62df34e0..85b2590d3aa 100644 --- a/qa/spec/tools/reliable_report_spec.rb +++ b/qa/spec/tools/reliable_report_spec.rb @@ -167,7 +167,7 @@ describe QA::Tools::ReliableReport do payload: { title: "Reliable e2e test report", description: issue_body, - labels: "Quality,test,type::maintenance,reliable test report" + labels: "Quality,test,type::maintenance,reliable test report,automation:devops-mapping-disable" } ) expect(slack_notifier).to have_received(:post).with( diff --git a/qa/spec/tools/test_resources_data_processor_spec.rb b/qa/spec/tools/test_resources_data_processor_spec.rb index 6a8c0fd06a4..5117d1d367f 100644 --- a/qa/spec/tools/test_resources_data_processor_spec.rb +++ b/qa/spec/tools/test_resources_data_processor_spec.rb @@ -1,33 +1,60 @@ # frozen_string_literal: true +require 'active_support/testing/time_helpers' + RSpec.describe QA::Tools::TestResourceDataProcessor do + include QA::Support::Helpers::StubEnv + include ActiveSupport::Testing::TimeHelpers + + subject(:processor) { Class.new(described_class).instance } + let(:info) { 'information' } - let(:api_path) { '/foo' } - let(:result) { [{ info: info, api_path: api_path }] } + let(:api_response) { {} } + let(:method) { :api } + let(:time) { 2 } + let(:api_path) { resource.api_delete_path } + let(:resource) { QA::Resource::Project.init { |project| project.id = 1 } } - describe '.collect' do - context 'when resource is not restricted' do - let(:resource) { instance_double(QA::Resource::Project, api_delete_path: '/foo', api_response: 'foo') } + let(:result) do + { + 'QA::Resource::Project' => [{ + info: info, + api_path: api_path, + fabrication_method: method, + fabrication_time: time, + http_method: :post, + timestamp: Time.now.to_s + }] + } + end + + before do + processor.collect(resource: resource, info: info, fabrication_method: method, fabrication_time: time) + end + + around do |example| + freeze_time { example.run } + end - it 'collects resource' do - expect(described_class.collect(resource, info)).to eq(result) - end + describe '.collect' do + it 'collects and stores resource' do + expect(processor.resources).to eq(result) end + end + + describe '.write_to_file' do + let(:resources_file) { Pathname.new(Faker::File.file_name(dir: 'tmp', ext: 'json')) } - context 'when resource api response is nil' do - let(:resource) { double(QA::Resource::Project, api_delete_path: '/foo', api_response: nil) } + before do + stub_env('QA_TEST_RESOURCES_CREATED_FILEPATH', resources_file) - it 'does not collect resource' do - expect(described_class.collect(resource, info)).to eq(nil) - end + allow(File).to receive(:write) end - context 'when resource is restricted' do - let(:resource) { double(QA::Resource::Sandbox, api_delete_path: '/foo', api_response: 'foo') } + it 'writes applicable resources to file' do + processor.write_to_file - it 'does not collect resource' do - expect(described_class.collect(resource, info)).to eq(nil) - end + expect(File).to have_received(:write).with(resources_file, JSON.pretty_generate(result)) end end end |