diff options
Diffstat (limited to 'qa')
101 files changed, 1930 insertions, 477 deletions
diff --git a/qa/Dockerfile b/qa/Dockerfile index 5d046636984..9611b3653eb 100644 --- a/qa/Dockerfile +++ b/qa/Dockerfile @@ -1,12 +1,15 @@ ARG DOCKER_VERSION=20.10.14 ARG CHROME_VERSION=101 +ARG QA_BUILD_TARGET=qa -FROM registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23 +FROM registry.gitlab.com/gitlab-org/gitlab-build-images/debian-bullseye-ruby-2.7:bundler-2.3-git-2.33-lfs-2.9-chrome-${CHROME_VERSION}-docker-${DOCKER_VERSION}-gcloud-383-kubectl-1.23 AS qa LABEL maintainer="GitLab Quality Department <quality@gitlab.com>" ENV DEBIAN_FRONTEND="noninteractive" # Override config path to make sure local config doesn't override it when building image locally ENV BUNDLE_APP_CONFIG=/home/gitlab/.bundle +# Use webdriver preinstalled in the base image +ENV WD_INSTALL_DIR=/usr/local/bin ## # Install system libs @@ -34,20 +37,13 @@ COPY ./qa/Gemfile* /home/gitlab/qa/ RUN bundle config set --local without development \ && bundle install --retry=3 -## -# Fetch chromedriver based on version of chrome -# Copy rakefile first so that webdriver is not reinstalled on every code change -# https://github.com/titusfortner/webdrivers -# -COPY ./qa/tasks/webdrivers.rake /home/gitlab/qa/tasks/ -RUN bundle exec rake -f tasks/webdrivers.rake webdrivers:chromedriver:update - COPY ./config/initializers/0_inject_enterprise_edition_module.rb /home/gitlab/config/initializers/ # Copy VERSION to ensure the COPY succeeds to copy at least one file since ee/app/models/license.rb isn't present in FOSS # The [b] part makes ./ee/app/models/license.r[b] a pattern that is allowed to return no files (which is the case in FOSS) COPY VERSION ./ee/app/models/license.r[b] /home/gitlab/ee/app/models/ -COPY ./config/bundler_setup.rb /home/gitlab/config/ +COPY VERSION ./ee/config/feature_flag[s] /home/gitlab/ee/config/feature_flags/ COPY ./config/feature_flags /home/gitlab/config/feature_flags +COPY ./config/bundler_setup.rb /home/gitlab/config/ COPY ./lib/gitlab_edition.rb /home/gitlab/lib/ COPY ./lib/gitlab/utils.rb /home/gitlab/lib/gitlab/ COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/ @@ -55,3 +51,11 @@ COPY ./INSTALLATION_TYPE ./VERSION /home/gitlab/ COPY ./qa /home/gitlab/qa ENTRYPOINT ["bin/test"] + +# Add JH files when pass the parameter: `--build-arg QA_BUILD_TARGET=jhqa` +FROM qa AS jhqa +ONBUILD COPY ./jh/qa /home/gitlab/jh/qa +ONBUILD COPY ./jh/lib /home/gitlab/jh/lib +ONBUILD COPY ./jh/config/feature_flags /home/gitlab/jh/config/feature_flags + +FROM $QA_BUILD_TARGET diff --git a/qa/lib/gitlab/page/admin/subscription.rb b/qa/lib/gitlab/page/admin/subscription.rb index b90a49abf4b..1538384f6ed 100644 --- a/qa/lib/gitlab/page/admin/subscription.rb +++ b/qa/lib/gitlab/page/admin/subscription.rb @@ -10,6 +10,8 @@ module Gitlab text_field :activation_code button :activate label :terms_of_services, text: /I agree that/ + link :remove_license, 'data-testid': 'license-remove-action' + button :confirm_ok_button p :plan p :started p :name @@ -21,6 +23,9 @@ module Gitlab h2 :users_over_subscription table :subscription_history + span :no_valid_license_alert, text: /no longer has a valid license/ + h3 :no_active_subscription_title, text: /do not have an active subscription/ + def accept_terms terms_of_services_element.click # workaround for hidden checkbox end @@ -39,7 +44,7 @@ module Gitlab # @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 + # @return [Boolean] True if record exists, 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| @@ -17,10 +17,16 @@ require 'active_support/core_ext/hash' require 'active_support/core_ext/object/blank' require 'rainbow/refinement' +require_relative 'qa/support/fips' + module QA root = "#{__dir__}/qa" loader = Zeitwerk::Loader.new + + # require jh/qa/qa.rb first, to load JH module make prepend module works + require '../jh/qa/qa' if GitlabEdition.jh? + loader.push_dir(root, namespace: QA) loader.ignore("#{root}/specs/features") diff --git a/qa/qa/fixtures/software_licenses/GFDL-1.2-only b/qa/qa/fixtures/software_licenses/GFDL-1.2-only new file mode 100644 index 00000000000..4421c2a496a --- /dev/null +++ b/qa/qa/fixtures/software_licenses/GFDL-1.2-only @@ -0,0 +1,397 @@ +GNU Free Documentation License +Version 1.2, November 2002 + + +Copyright (C) 2000,2001,2002 Free Software Foundation, Inc. +51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +Everyone is permitted to copy and distribute verbatim copies +of this license document, but changing it is not allowed. + + +0. PREAMBLE + +The purpose of this License is to make a manual, textbook, or other +functional and useful document "free" in the sense of freedom: to +assure everyone the effective freedom to copy and redistribute it, +with or without modifying it, either commercially or noncommercially. +Secondarily, this License preserves for the author and publisher a way +to get credit for their work, while not being considered responsible +for modifications made by others. + +This License is a kind of "copyleft", which means that derivative +works of the document must themselves be free in the same sense. It +complements the GNU General Public License, which is a copyleft +license designed for free software. + +We have designed this License in order to use it for manuals for free +software, because free software needs free documentation: a free +program should come with manuals providing the same freedoms that the +software does. But this License is not limited to software manuals; +it can be used for any textual work, regardless of subject matter or +whether it is published as a printed book. We recommend this License +principally for works whose purpose is instruction or reference. + + +1. APPLICABILITY AND DEFINITIONS + +This License applies to any manual or other work, in any medium, that +contains a notice placed by the copyright holder saying it can be +distributed under the terms of this License. Such a notice grants a +world-wide, royalty-free license, unlimited in duration, to use that +work under the conditions stated herein. The "Document", below, +refers to any such manual or work. Any member of the public is a +licensee, and is addressed as "you". You accept the license if you +copy, modify or distribute the work in a way requiring permission +under copyright law. + +A "Modified Version" of the Document means any work containing the +Document or a portion of it, either copied verbatim, or with +modifications and/or translated into another language. + +A "Secondary Section" is a named appendix or a front-matter section of +the Document that deals exclusively with the relationship of the +publishers or authors of the Document to the Document's overall subject +(or to related matters) and contains nothing that could fall directly +within that overall subject. (Thus, if the Document is in part a +textbook of mathematics, a Secondary Section may not explain any +mathematics.) The relationship could be a matter of historical +connection with the subject or with related matters, or of legal, +commercial, philosophical, ethical or political position regarding +them. + +The "Invariant Sections" are certain Secondary Sections whose titles +are designated, as being those of Invariant Sections, in the notice +that says that the Document is released under this License. If a +section does not fit the above definition of Secondary then it is not +allowed to be designated as Invariant. The Document may contain zero +Invariant Sections. If the Document does not identify any Invariant +Sections then there are none. + +The "Cover Texts" are certain short passages of text that are listed, +as Front-Cover Texts or Back-Cover Texts, in the notice that says that +the Document is released under this License. A Front-Cover Text may +be at most 5 words, and a Back-Cover Text may be at most 25 words. + +A "Transparent" copy of the Document means a machine-readable copy, +represented in a format whose specification is available to the +general public, that is suitable for revising the document +straightforwardly with generic text editors or (for images composed of +pixels) generic paint programs or (for drawings) some widely available +drawing editor, and that is suitable for input to text formatters or +for automatic translation to a variety of formats suitable for input +to text formatters. A copy made in an otherwise Transparent file +format whose markup, or absence of markup, has been arranged to thwart +or discourage subsequent modification by readers is not Transparent. +An image format is not Transparent if used for any substantial amount +of text. A copy that is not "Transparent" is called "Opaque". + +Examples of suitable formats for Transparent copies include plain +ASCII without markup, Texinfo input format, LaTeX input format, SGML +or XML using a publicly available DTD, and standard-conforming simple +HTML, PostScript or PDF designed for human modification. Examples of +transparent image formats include PNG, XCF and JPG. Opaque formats +include proprietary formats that can be read and edited only by +proprietary word processors, SGML or XML for which the DTD and/or +processing tools are not generally available, and the +machine-generated HTML, PostScript or PDF produced by some word +processors for output purposes only. + +The "Title Page" means, for a printed book, the title page itself, +plus such following pages as are needed to hold, legibly, the material +this License requires to appear in the title page. For works in +formats which do not have any title page as such, "Title Page" means +the text near the most prominent appearance of the work's title, +preceding the beginning of the body of the text. + +A section "Entitled XYZ" means a named subunit of the Document whose +title either is precisely XYZ or contains XYZ in parentheses following +text that translates XYZ in another language. (Here XYZ stands for a +specific section name mentioned below, such as "Acknowledgements", +"Dedications", "Endorsements", or "History".) To "Preserve the Title" +of such a section when you modify the Document means that it remains a +section "Entitled XYZ" according to this definition. + +The Document may include Warranty Disclaimers next to the notice which +states that this License applies to the Document. These Warranty +Disclaimers are considered to be included by reference in this +License, but only as regards disclaiming warranties: any other +implication that these Warranty Disclaimers may have is void and has +no effect on the meaning of this License. + + +2. VERBATIM COPYING + +You may copy and distribute the Document in any medium, either +commercially or noncommercially, provided that this License, the +copyright notices, and the license notice saying this License applies +to the Document are reproduced in all copies, and that you add no other +conditions whatsoever to those of this License. You may not use +technical measures to obstruct or control the reading or further +copying of the copies you make or distribute. However, you may accept +compensation in exchange for copies. If you distribute a large enough +number of copies you must also follow the conditions in section 3. + +You may also lend copies, under the same conditions stated above, and +you may publicly display copies. + + +3. COPYING IN QUANTITY + +If you publish printed copies (or copies in media that commonly have +printed covers) of the Document, numbering more than 100, and the +Document's license notice requires Cover Texts, you must enclose the +copies in covers that carry, clearly and legibly, all these Cover +Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on +the back cover. Both covers must also clearly and legibly identify +you as the publisher of these copies. The front cover must present +the full title with all words of the title equally prominent and +visible. You may add other material on the covers in addition. +Copying with changes limited to the covers, as long as they preserve +the title of the Document and satisfy these conditions, can be treated +as verbatim copying in other respects. + +If the required texts for either cover are too voluminous to fit +legibly, you should put the first ones listed (as many as fit +reasonably) on the actual cover, and continue the rest onto adjacent +pages. + +If you publish or distribute Opaque copies of the Document numbering +more than 100, you must either include a machine-readable Transparent +copy along with each Opaque copy, or state in or with each Opaque copy +a computer-network location from which the general network-using +public has access to download using public-standard network protocols +a complete Transparent copy of the Document, free of added material. +If you use the latter option, you must take reasonably prudent steps, +when you begin distribution of Opaque copies in quantity, to ensure +that this Transparent copy will remain thus accessible at the stated +location until at least one year after the last time you distribute an +Opaque copy (directly or through your agents or retailers) of that +edition to the public. + +It is requested, but not required, that you contact the authors of the +Document well before redistributing any large number of copies, to give +them a chance to provide you with an updated version of the Document. + + +4. MODIFICATIONS + +You may copy and distribute a Modified Version of the Document under +the conditions of sections 2 and 3 above, provided that you release +the Modified Version under precisely this License, with the Modified +Version filling the role of the Document, thus licensing distribution +and modification of the Modified Version to whoever possesses a copy +of it. In addition, you must do these things in the Modified Version: + +A. Use in the Title Page (and on the covers, if any) a title distinct +from that of the Document, and from those of previous versions +(which should, if there were any, be listed in the History section +of the Document). You may use the same title as a previous version +if the original publisher of that version gives permission. +B. List on the Title Page, as authors, one or more persons or entities +responsible for authorship of the modifications in the Modified +Version, together with at least five of the principal authors of the +Document (all of its principal authors, if it has fewer than five), +unless they release you from this requirement. +C. State on the Title page the name of the publisher of the +Modified Version, as the publisher. +D. Preserve all the copyright notices of the Document. +E. Add an appropriate copyright notice for your modifications +adjacent to the other copyright notices. +F. Include, immediately after the copyright notices, a license notice +giving the public permission to use the Modified Version under the +terms of this License, in the form shown in the Addendum below. +G. Preserve in that license notice the full lists of Invariant Sections +and required Cover Texts given in the Document's license notice. +H. Include an unaltered copy of this License. +I. Preserve the section Entitled "History", Preserve its Title, and add +to it an item stating at least the title, year, new authors, and +publisher of the Modified Version as given on the Title Page. If +there is no section Entitled "History" in the Document, create one +stating the title, year, authors, and publisher of the Document as +given on its Title Page, then add an item describing the Modified +Version as stated in the previous sentence. +J. Preserve the network location, if any, given in the Document for +public access to a Transparent copy of the Document, and likewise +the network locations given in the Document for previous versions +it was based on. These may be placed in the "History" section. +You may omit a network location for a work that was published at +least four years before the Document itself, or if the original +publisher of the version it refers to gives permission. +K. For any section Entitled "Acknowledgements" or "Dedications", +Preserve the Title of the section, and preserve in the section all +the substance and tone of each of the contributor acknowledgements +and/or dedications given therein. +L. Preserve all the Invariant Sections of the Document, +unaltered in their text and in their titles. Section numbers +or the equivalent are not considered part of the section titles. +M. Delete any section Entitled "Endorsements". Such a section +may not be included in the Modified Version. +N. Do not retitle any existing section to be Entitled "Endorsements" +or to conflict in title with any Invariant Section. +O. Preserve any Warranty Disclaimers. + +If the Modified Version includes new front-matter sections or +appendices that qualify as Secondary Sections and contain no material +copied from the Document, you may at your option designate some or all +of these sections as invariant. To do this, add their titles to the +list of Invariant Sections in the Modified Version's license notice. +These titles must be distinct from any other section titles. + +You may add a section Entitled "Endorsements", provided it contains +nothing but endorsements of your Modified Version by various +parties--for example, statements of peer review or that the text has +been approved by an organization as the authoritative definition of a +standard. + +You may add a passage of up to five words as a Front-Cover Text, and a +passage of up to 25 words as a Back-Cover Text, to the end of the list +of Cover Texts in the Modified Version. Only one passage of +Front-Cover Text and one of Back-Cover Text may be added by (or +through arrangements made by) any one entity. If the Document already +includes a cover text for the same cover, previously added by you or +by arrangement made by the same entity you are acting on behalf of, +you may not add another; but you may replace the old one, on explicit +permission from the previous publisher that added the old one. + +The author(s) and publisher(s) of the Document do not by this License +give permission to use their names for publicity for or to assert or +imply endorsement of any Modified Version. + + +5. COMBINING DOCUMENTS + +You may combine the Document with other documents released under this +License, under the terms defined in section 4 above for modified +versions, provided that you include in the combination all of the +Invariant Sections of all of the original documents, unmodified, and +list them all as Invariant Sections of your combined work in its +license notice, and that you preserve all their Warranty Disclaimers. + +The combined work need only contain one copy of this License, and +multiple identical Invariant Sections may be replaced with a single +copy. If there are multiple Invariant Sections with the same name but +different contents, make the title of each such section unique by +adding at the end of it, in parentheses, the name of the original +author or publisher of that section if known, or else a unique number. +Make the same adjustment to the section titles in the list of +Invariant Sections in the license notice of the combined work. + +In the combination, you must combine any sections Entitled "History" +in the various original documents, forming one section Entitled +"History"; likewise combine any sections Entitled "Acknowledgements", +and any sections Entitled "Dedications". You must delete all sections +Entitled "Endorsements". + + +6. COLLECTIONS OF DOCUMENTS + +You may make a collection consisting of the Document and other documents +released under this License, and replace the individual copies of this +License in the various documents with a single copy that is included in +the collection, provided that you follow the rules of this License for +verbatim copying of each of the documents in all other respects. + +You may extract a single document from such a collection, and distribute +it individually under this License, provided you insert a copy of this +License into the extracted document, and follow this License in all +other respects regarding verbatim copying of that document. + + +7. AGGREGATION WITH INDEPENDENT WORKS + +A compilation of the Document or its derivatives with other separate +and independent documents or works, in or on a volume of a storage or +distribution medium, is called an "aggregate" if the copyright +resulting from the compilation is not used to limit the legal rights +of the compilation's users beyond what the individual works permit. +When the Document is included in an aggregate, this License does not +apply to the other works in the aggregate which are not themselves +derivative works of the Document. + +If the Cover Text requirement of section 3 is applicable to these +copies of the Document, then if the Document is less than one half of +the entire aggregate, the Document's Cover Texts may be placed on +covers that bracket the Document within the aggregate, or the +electronic equivalent of covers if the Document is in electronic form. +Otherwise they must appear on printed covers that bracket the whole +aggregate. + + +8. TRANSLATION + +Translation is considered a kind of modification, so you may +distribute translations of the Document under the terms of section 4. +Replacing Invariant Sections with translations requires special +permission from their copyright holders, but you may include +translations of some or all Invariant Sections in addition to the +original versions of these Invariant Sections. You may include a +translation of this License, and all the license notices in the +Document, and any Warranty Disclaimers, provided that you also include +the original English version of this License and the original versions +of those notices and disclaimers. In case of a disagreement between +the translation and the original version of this License or a notice +or disclaimer, the original version will prevail. + +If a section in the Document is Entitled "Acknowledgements", +"Dedications", or "History", the requirement (section 4) to Preserve +its Title (section 1) will typically require changing the actual +title. + + +9. TERMINATION + +You may not copy, modify, sublicense, or distribute the Document except +as expressly provided for under this License. Any other attempt to +copy, modify, sublicense or distribute the Document is void, and will +automatically terminate your rights under this License. However, +parties who have received copies, or rights, from you under this +License will not have their licenses terminated so long as such +parties remain in full compliance. + + +10. FUTURE REVISIONS OF THIS LICENSE + +The Free Software Foundation may publish new, revised versions +of the GNU Free Documentation License from time to time. Such new +versions will be similar in spirit to the present version, but may +differ in detail to address new problems or concerns. See +https://www.gnu.org/licenses/. + +Each version of the License is given a distinguishing version number. +If the Document specifies that a particular numbered version of this +License "or any later version" applies to it, you have the option of +following the terms and conditions either of that specified version or +of any later version that has been published (not as a draft) by the +Free Software Foundation. If the Document does not specify a version +number of this License, you may choose any version ever published (not +as a draft) by the Free Software Foundation. + + +ADDENDUM: How to use this License for your documents + +To use this License in a document you have written, include a copy of +the License in the document and put the following copyright and +license notices just after the title page: + +Copyright (c) YEAR YOUR NAME. +Permission is granted to copy, distribute and/or modify this document +under the terms of the GNU Free Documentation License, Version 1.2 +or any later version published by the Free Software Foundation; +with no Invariant Sections, no Front-Cover Texts, and no Back-Cover Texts. +A copy of the license is included in the section entitled "GNU +Free Documentation License". + +If you have Invariant Sections, Front-Cover Texts and Back-Cover Texts, +replace the "with...Texts." line with this: + +with the Invariant Sections being LIST THEIR TITLES, with the +Front-Cover Texts being LIST, and with the Back-Cover Texts being LIST. + +If you have Invariant Sections without Cover Texts, or some other +combination of the three, merge those two alternatives to suit the +situation. + +If your document contains nontrivial examples of program code, we +recommend releasing these examples in parallel under your choice of +free software license, such as the GNU General Public License, +to permit their use in free software. diff --git a/qa/qa/fixtures/software_licenses/bsd-3-clause b/qa/qa/fixtures/software_licenses/bsd-3-clause new file mode 100644 index 00000000000..3deb7c523fb --- /dev/null +++ b/qa/qa/fixtures/software_licenses/bsd-3-clause @@ -0,0 +1,11 @@ +Copyright 2022 Joe Bloggs + +Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/qa/qa/flow/login.rb b/qa/qa/flow/login.rb index b60f74fe9bf..05f114acbc5 100644 --- a/qa/qa/flow/login.rb +++ b/qa/qa/flow/login.rb @@ -40,8 +40,14 @@ module QA sign_in(as: Runtime::User.admin, address: address, admin: true) end - def sign_in_unless_signed_in(as: nil, address: :gitlab) - sign_in(as: as, address: address) unless Page::Main::Menu.perform(&:signed_in?) + def sign_in_unless_signed_in(user: nil, address: :gitlab) + if user + sign_in(as: user, address: address) unless Page::Main::Menu.perform do |menu| + menu.signed_in_as_user?(user) + end + else + sign_in(address: address) unless Page::Main::Menu.perform(&:signed_in?) + end end end end diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb index 2244a78b9e5..f132d7b7885 100644 --- a/qa/qa/git/repository.rb +++ b/qa/qa/git/repository.rb @@ -163,7 +163,7 @@ module QA ssh end - env_vars << %(GIT_SSH_COMMAND="ssh -i #{ssh.private_key_file.path} -o UserKnownHostsFile=#{ssh.known_hosts_file.path}") + env_vars << %(GIT_SSH_COMMAND="ssh -i #{ssh.private_key_file.path} -o UserKnownHostsFile=#{ssh.known_hosts_file.path} -o IdentitiesOnly=yes") end def delete_ssh_key @@ -218,6 +218,31 @@ module QA run_git('git --no-pager branch --list --remotes --format="%(refname:lstrip=3)"').to_s.split("\n") end + # Gets the size of the repository using `git rev-list --all --objects --use-bitmap-index --disk-usage` as + # Gitaly does (see https://gitlab.com/gitlab-org/gitlab/-/issues/357680) + def local_size + internal_refs = %w[ + refs/keep-around/ + refs/merge-requests/ + refs/pipelines/ + refs/remotes/ + refs/tmp/ + refs/environments/ + ] + cmd = <<~CMD + git rev-list #{internal_refs.map { |r| "--exclude='#{r}*'" }.join(' ')} \ + --not --alternate-refs --not \ + --all --objects --use-bitmap-index --disk-usage + CMD + + run_git(cmd).to_i + end + + # Performs garbage collection + def run_gc + run_git('git gc') + end + private attr_reader :uri, :username, :password, :ssh, :use_lfs diff --git a/qa/qa/page/alert/free_trial.rb b/qa/qa/page/alert/free_trial.rb index 4a48d4ca277..ec8d8e8abde 100644 --- a/qa/qa/page/alert/free_trial.rb +++ b/qa/qa/page/alert/free_trial.rb @@ -4,8 +4,7 @@ module QA module Page module Alert class FreeTrial < Chemlab::Page - # TODO: Supplant with data-qa-selectors - h4 :trial_activated_message, class: 'gl-banner-title' + element :trial_activated_message end end end diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb index 775a5ead5f7..d7e0101ff2c 100644 --- a/qa/qa/page/base.rb +++ b/qa/qa/page/base.rb @@ -53,8 +53,20 @@ module QA wait_for_requests(skip_finished_loading_check: skip_finished_loading_check) end - def wait_until(max_duration: 60, sleep_interval: 0.1, reload: true, raise_on_failure: true, skip_finished_loading_check_on_refresh: false) - Support::Waiter.wait_until(max_duration: max_duration, sleep_interval: sleep_interval, raise_on_failure: raise_on_failure) do + def wait_until( + max_duration: 60, + sleep_interval: 0.1, + reload: true, + raise_on_failure: true, + skip_finished_loading_check_on_refresh: false, + message: nil + ) + Support::Waiter.wait_until( + max_duration: max_duration, + sleep_interval: sleep_interval, + raise_on_failure: raise_on_failure, + message: message + ) do yield || (reload && refresh(skip_finished_loading_check: skip_finished_loading_check_on_refresh) && false) end end @@ -80,7 +92,7 @@ module QA ) end - def scroll_to(selector, text: nil) + def scroll_to(selector, text: nil, &block) wait_for_requests page.execute_script <<~JS @@ -94,7 +106,7 @@ module QA } JS - page.within(selector) { yield } if block_given? + page.within(selector, &block) if block end # Returns true if successfully GETs the given URL diff --git a/qa/qa/page/blame/show.rb b/qa/qa/page/blame/show.rb new file mode 100644 index 00000000000..cbe8ef600dc --- /dev/null +++ b/qa/qa/page/blame/show.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +module QA + module Page + module Blame + class Show < Page::Base + view 'app/views/projects/blob/_header_content.html.haml' do + element :file_name_content + end + + view 'app/views/projects/blame/show.html.haml' do + element :blame_file_content + end + + def has_file?(file_name) + within_element(:file_name_content) { has_text?(file_name) } + end + + def has_no_file?(file_name) + within_element(:file_name_content) do + has_no_text?(file_name) + end + end + + def has_file_content?(file_content) + within_element(:blame_file_content) { has_text?(file_content) } + end + + def has_no_file_content?(file_content) + within_element(:blame_file_content) do + has_no_text?(file_content) + end + end + end + end + end +end diff --git a/qa/qa/page/component/access_tokens.rb b/qa/qa/page/component/access_tokens.rb index 28ca77aba24..f143e5b9e1f 100644 --- a/qa/qa/page/component/access_tokens.rb +++ b/qa/qa/page/component/access_tokens.rb @@ -1,4 +1,5 @@ # frozen_string_literal: true +require 'date' module QA module Page diff --git a/qa/qa/page/component/blob_content.rb b/qa/qa/page/component/blob_content.rb index c2a1687ccfc..b6001cf39b5 100644 --- a/qa/qa/page/component/blob_content.rb +++ b/qa/qa/page/component/blob_content.rb @@ -25,14 +25,6 @@ module QA 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 - - base.view 'app/views/shared/_file_highlight.html.haml' do - element :file_content - end end def has_file?(name) @@ -44,21 +36,21 @@ module QA end def has_file_name?(file_name, file_number = nil) - within_file_by_number(file_name_element, file_number) { has_text?(file_name) } + within_file_by_number(:file_title_content, file_number) { has_text?(file_name) } end def has_no_file_name?(file_name) - within_element(file_name_element) do + within_element(:file_title_content) do has_no_text?(file_name) end end def has_file_content?(file_content, file_number = nil) - within_file_by_number(file_content_element, file_number) { has_text?(file_content) } + within_file_by_number(:blob_viewer_file_content, file_number) { has_text?(file_content) } end def has_no_file_content?(file_content) - within_element(file_content_element) do + within_element(:blob_viewer_file_content) do has_no_text?(file_content) end end @@ -80,14 +72,6 @@ module QA private - def file_content_element - feature_flag_controlled_element(:refactor_blob_viewer, :blob_viewer_file_content, :file_content) - end - - def file_name_element - feature_flag_controlled_element(:refactor_blob_viewer, :file_title_content, :file_name_content) - end - def within_file_by_number(element, file_number) if file_number within_element_by_index(element, file_number - 1) { yield } diff --git a/qa/qa/page/component/design_management.rb b/qa/qa/page/component/design_management.rb index 73ba5713bda..90c86688882 100644 --- a/qa/qa/page/component/design_management.rb +++ b/qa/qa/page/component/design_management.rb @@ -54,8 +54,9 @@ module QA # We'll check for the annotation in a test, but here we'll at least # wait for the "Save comment" button to disappear saved = has_no_element?(:save_comment_button) + return if saved - raise RSpec::Expectations::ExpectationNotMetError, %q(There was a problem while adding the annotation) unless saved + raise RSpec::Expectations::ExpectationNotMetError, %q(There was a problem while adding the annotation) end def add_design(design_file_path) @@ -69,15 +70,11 @@ module QA filename = ::File.basename(design_file_path) - found = wait_until(reload: false, sleep_interval: 1) do + wait_until(reload: false, sleep_interval: 1, message: "Design upload") do image = find_element(:design_image, filename: filename) - has_element?(:design_file_name, text: filename) && - image["complete"] && - image["naturalWidth"].to_i > 0 + has_element?(:design_file_name, text: filename) && image["complete"] && image["naturalWidth"].to_i > 0 end - - raise ElementNotFound, %Q(Attempted to attach design "#{filename}" but it did not appear) unless found end def update_design(filename) diff --git a/qa/qa/page/component/invite_members_modal.rb b/qa/qa/page/component/invite_members_modal.rb index 18747c19aee..5c39cfd3695 100644 --- a/qa/qa/page/component/invite_members_modal.rb +++ b/qa/qa/page/component/invite_members_modal.rb @@ -17,6 +17,7 @@ module QA base.view 'app/assets/javascripts/invite_members/components/group_select.vue' do element :group_select_dropdown_search_field + element :group_select_dropdown_item end base.view 'app/assets/javascripts/invite_members/components/members_token_select.vue' do @@ -59,12 +60,16 @@ module QA within_element(:invite_members_modal_content) do click_button 'Select a group' - # Helps stabilize race condition with concurrent group API calls while searching - # TODO: Replace with `fill_element :group_select_dropdown_search_field, group_name` when this bug is resolved: https://gitlab.com/gitlab-org/gitlab/-/issues/349379 - send_keys_to_element(:group_select_dropdown_search_field, group_name) + Support::Waiter.wait_until { has_element?(:group_select_dropdown_item) } + + # Workaround for race condition with concurrent group API calls while searching + # Remove Retrier after https://gitlab.com/gitlab-org/gitlab/-/issues/349379 is resolved + Support::Retrier.retry_on_exception do + fill_element :group_select_dropdown_search_field, group_name + Support::WaitForRequests.wait_for_requests + click_button group_name + end - Support::WaitForRequests.wait_for_requests - click_button group_name set_access_level(access_level) end diff --git a/qa/qa/page/component/issuable/common.rb b/qa/qa/page/component/issuable/common.rb index 0d056afd7f0..768a15a4551 100644 --- a/qa/qa/page/component/issuable/common.rb +++ b/qa/qa/page/component/issuable/common.rb @@ -10,22 +10,8 @@ module QA def self.included(base) super - base.view 'app/assets/javascripts/issues/show/components/title.vue' do - element :edit_button - element :title, required: true - end - - base.view 'app/assets/javascripts/issues/show/components/fields/title.vue' do - element :title_input - end - - base.view 'app/assets/javascripts/issues/show/components/fields/description.vue' do - element :description_textarea - end - - base.view 'app/assets/javascripts/issues/show/components/edit_actions.vue' do - element :save_button - element :delete_button + base.view 'app/assets/javascripts/vue_shared/issuable/show/components/issuable_title.vue' do + element :title_content, required: true end end end diff --git a/qa/qa/page/component/issuable/sidebar.rb b/qa/qa/page/component/issuable/sidebar.rb index 921647eb4cc..4131731111f 100644 --- a/qa/qa/page/component/issuable/sidebar.rb +++ b/qa/qa/page/component/issuable/sidebar.rb @@ -117,7 +117,7 @@ module QA end end - click_element(:title) # to blur dropdown + click_element(:title_content) # to blur dropdown end def toggle_more_assignees_link diff --git a/qa/qa/page/component/note.rb b/qa/qa/page/component/note.rb index 7c733a231f1..8494518723d 100644 --- a/qa/qa/page/component/note.rb +++ b/qa/qa/page/component/note.rb @@ -168,7 +168,7 @@ module QA def select_filter_with_text(text) retry_on_exception do - click_element(:title) + click_element(:title_content) click_element :discussion_filter_dropdown find_element(:filter_menu_item, text: text).click diff --git a/qa/qa/page/component/visibility_setting.rb b/qa/qa/page/component/visibility_setting.rb index 4370cfb4564..f07b9c1c455 100644 --- a/qa/qa/page/component/visibility_setting.rb +++ b/qa/qa/page/component/visibility_setting.rb @@ -15,7 +15,7 @@ module QA end def set_visibility(visibility) - choose_element("#{visibility.downcase}_radio", false, true) + find('label', text: visibility.capitalize).click end end end diff --git a/qa/qa/page/file/show.rb b/qa/qa/page/file/show.rb index 7d6d81cf869..581aa835ada 100644 --- a/qa/qa/page/file/show.rb +++ b/qa/qa/page/file/show.rb @@ -19,10 +19,6 @@ module QA element :delete_button, '_("Delete")' # rubocop:disable QA/ElementWithPattern end - view 'app/views/projects/blob/_remove.html.haml' do - element :delete_file_button, "button_tag 'Delete file'" # rubocop:disable QA/ElementWithPattern - end - view 'app/assets/javascripts/vue_shared/components/web_ide_link.vue' do element :edit_button end diff --git a/qa/qa/page/group/menu.rb b/qa/qa/page/group/menu.rb index be877e56713..783fbd25929 100644 --- a/qa/qa/page/group/menu.rb +++ b/qa/qa/page/group/menu.rb @@ -76,6 +76,14 @@ module QA end end + def go_to_access_token_settings + hover_group_settings do + within_submenu do + click_element(:sidebar_menu_item_link, menu_item: 'Access Tokens') + end + end + end + private def hover_settings diff --git a/qa/qa/page/group/new.rb b/qa/qa/page/group/new.rb index c24712b9418..09a9af7aaf7 100644 --- a/qa/qa/page/group/new.rb +++ b/qa/qa/page/group/new.rb @@ -6,7 +6,7 @@ module QA class New < Page::Base include Page::Component::VisibilitySetting - view 'app/views/shared/_group_form.html.haml' do + view 'app/assets/javascripts/groups/components/group_name_and_path.vue' do element :group_path_field element :group_name_field end diff --git a/qa/qa/page/group/settings/access_tokens.rb b/qa/qa/page/group/settings/access_tokens.rb new file mode 100644 index 00000000000..705988285ce --- /dev/null +++ b/qa/qa/page/group/settings/access_tokens.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module QA + module Page + module Group + module Settings + class AccessTokens < Page::Base + include Page::Component::AccessTokens + include Page::Component::ConfirmModal + end + end + end + end +end diff --git a/qa/qa/page/group/settings/package_registries.rb b/qa/qa/page/group/settings/package_registries.rb index 433872a378a..35186159dc9 100644 --- a/qa/qa/page/group/settings/package_registries.rb +++ b/qa/qa/page/group/settings/package_registries.rb @@ -8,8 +8,8 @@ module QA view 'app/assets/javascripts/packages_and_registries/settings/group/components/packages_settings.vue' do element :package_registry_settings_content - element :allow_duplicates_toggle - element :allow_duplicates_label + element :reject_duplicates_toggle + element :reject_duplicates_label end view 'app/assets/javascripts/packages_and_registries/settings/group/components/dependency_proxy_settings.vue' do @@ -17,39 +17,39 @@ module QA element :dependency_proxy_setting_toggle end - def set_allow_duplicates_disabled - expand_content :package_registry_settings_content do - click_on_allow_duplicates_button if duplicates_enabled? + def set_reject_duplicates_disabled + within_element :package_registry_settings_content do + click_on_reject_duplicates_button if duplicates_disabled? end end - def set_allow_duplicates_enabled - expand_content :package_registry_settings_content do - click_on_allow_duplicates_button unless duplicates_enabled? + def set_reject_duplicates_enabled + within_element :package_registry_settings_content do + click_on_reject_duplicates_button unless duplicates_disabled? end end - def click_on_allow_duplicates_button - with_allow_duplicates_button do |button| + def click_on_reject_duplicates_button + with_reject_duplicates_button do |button| button.click end end - def duplicates_enabled? - with_allow_duplicates_button do |button| + def duplicates_disabled? + with_reject_duplicates_button do |button| button[:class].include?('is-checked') end end - def with_allow_duplicates_button - within_element :allow_duplicates_toggle do + def with_reject_duplicates_button + within_element :reject_duplicates_toggle do toggle = find('button.gl-toggle:not(.is-disabled)') yield(toggle) end end def has_dependency_proxy_enabled? - expand_content :dependency_proxy_settings_content do + within_element :dependency_proxy_settings_content do within_element :dependency_proxy_setting_toggle do toggle = find('button.gl-toggle') toggle[:class].include?('is-checked') diff --git a/qa/qa/page/issuable/new.rb b/qa/qa/page/issuable/new.rb index ca1219cb7fc..0c95f722080 100644 --- a/qa/qa/page/issuable/new.rb +++ b/qa/qa/page/issuable/new.rb @@ -13,7 +13,7 @@ module QA end view 'app/views/shared/form_elements/_description.html.haml' do - element :issuable_form_description + element :issuable_form_description_field end view 'app/views/shared/issuable/_milestone_dropdown.html.haml' do @@ -37,7 +37,7 @@ module QA end def fill_description(description) - fill_element :issuable_form_description, description + fill_element :issuable_form_description_field, description end def choose_milestone(milestone) diff --git a/qa/qa/page/main/menu.rb b/qa/qa/page/main/menu.rb index 0bb74455f81..90b419f8cef 100644 --- a/qa/qa/page/main/menu.rb +++ b/qa/qa/page/main/menu.rb @@ -113,6 +113,14 @@ module QA has_personal_area?(wait: 0) end + def signed_in_as_user?(user) + return false if has_no_personal_area? + + within_user_menu do + has_element?(:user_profile_link, text: /#{user.username}/) + end + end + def not_signed_in? return true if Page::Main::Login.perform(&:on_login_page?) @@ -202,7 +210,7 @@ module QA def within_user_menu(&block) within_top_menu do - click_element :user_avatar + click_element :user_avatar unless has_element?(:user_profile_link, wait: 1) within_element(:user_menu, &block) end diff --git a/qa/qa/page/mattermost/login.rb b/qa/qa/page/mattermost/login.rb index 38a88b42992..ab711746459 100644 --- a/qa/qa/page/mattermost/login.rb +++ b/qa/qa/page/mattermost/login.rb @@ -12,7 +12,7 @@ module QA view 'app/views/projects/mattermosts/new.html.haml' def sign_in_using_oauth - click_link class: 'btn btn-custom-login gitlab' + click_link 'gitlab' if page.has_content?('Authorize GitLab Mattermost to use your account?') click_button 'Authorize' diff --git a/qa/qa/page/merge_request/new.rb b/qa/qa/page/merge_request/new.rb index b19a0d1a830..79eb4f2d51b 100644 --- a/qa/qa/page/merge_request/new.rb +++ b/qa/qa/page/merge_request/new.rb @@ -8,10 +8,6 @@ module QA element :issuable_create_button, required: true end - view 'app/views/shared/form_elements/_description.html.haml' do - element :issuable_form_description - end - view 'app/views/projects/merge_requests/creations/_new_compare.html.haml' do element :compare_branches_button element :source_branch_dropdown @@ -40,10 +36,6 @@ module QA click_element(:issuable_create_button, Page::MergeRequest::Show) end - def has_description?(description) - has_element?(:issuable_form_description, text: description) - end - def click_diffs_tab click_element(:diffs_tab) click_element(:dismiss_popover_button) if has_element?(:dismiss_popover_button, wait: 1) diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb index a0bebf6bd7a..27c12a4e21f 100644 --- a/qa/qa/page/merge_request/show.rb +++ b/qa/qa/page/merge_request/show.rb @@ -222,7 +222,7 @@ module QA def has_pipeline_status?(text) # Pipelines can be slow, so we wait a bit longer than the usual 10 seconds wait_until(max_duration: 120, sleep_interval: 5, reload: true) do - has_element?(:merge_request_pipeline_info_content, text: text, wait: 15 ) + has_element?(:merge_request_pipeline_info_content, text: text, wait: 15) end end @@ -277,6 +277,11 @@ module QA has_element?(:merge_button, disabled: false) end + # Waits up 10 seconds and returns false if the Revert button is not enabled + def revertible? + has_element?(:revert_button, disabled: false, wait: 10) + end + # 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 @@ -318,11 +323,15 @@ module QA end def merge_immediately! - if has_element?(:merge_moment_dropdown) - click_element(:merge_moment_dropdown, skip_finished_loading_check: true) - click_element(:merge_immediately_menu_item, skip_finished_loading_check: true) - else - click_element(:merge_button, skip_finished_loading_check: true) + retry_until(reload: true, sleep_interval: 1, max_attempts: 12) do + if has_element?(:merge_moment_dropdown) + click_element(:merge_moment_dropdown, skip_finished_loading_check: true) + click_element(:merge_immediately_menu_item, skip_finished_loading_check: true) + else + click_element(:merge_button, skip_finished_loading_check: true) + end + + merged? end end diff --git a/qa/qa/page/project/issue/show.rb b/qa/qa/page/project/issue/show.rb index fe468de60cd..b1417d9b9db 100644 --- a/qa/qa/page/project/issue/show.rb +++ b/qa/qa/page/project/issue/show.rb @@ -5,7 +5,6 @@ module QA module Project module Issue class Show < Page::Base - include Page::Component::Issuable::Common include Page::Component::Note include Page::Component::DesignManagement include Page::Component::Issuable::Sidebar @@ -22,6 +21,10 @@ module QA element :delete_issue_button end + view 'app/assets/javascripts/issues/show/components/title.vue' do + element :title_content, required: true + end + view 'app/assets/javascripts/related_issues/components/add_issuable_form.vue' do element :add_issue_button end @@ -79,7 +82,10 @@ module QA def delete_issue click_element(:issue_actions_ellipsis_dropdown) - click_element(:delete_issue_button, Page::Modal::DeleteIssue) + + click_element(:delete_issue_button, + Page::Modal::DeleteIssue, + wait: Support::Repeater::DEFAULT_MAX_WAIT_TIME) Page::Modal::DeleteIssue.perform(&:confirm_delete_issue) diff --git a/qa/qa/page/project/new.rb b/qa/qa/page/project/new.rb index 7da763ca0e6..7864e664429 100644 --- a/qa/qa/page/project/new.rb +++ b/qa/qa/page/project/new.rb @@ -76,7 +76,7 @@ module QA end def set_visibility(visibility) - choose visibility.capitalize + find('label', text: visibility.capitalize).click end # Disable experiment for SAST at project creation https://gitlab.com/gitlab-org/gitlab/-/issues/333196 diff --git a/qa/qa/page/project/settings/access_tokens.rb b/qa/qa/page/project/settings/access_tokens.rb index 47afa26191c..69a8bdeaab8 100644 --- a/qa/qa/page/project/settings/access_tokens.rb +++ b/qa/qa/page/project/settings/access_tokens.rb @@ -1,7 +1,5 @@ # frozen_string_literal: true -require 'date' - module QA module Page module Project diff --git a/qa/qa/page/project/settings/deploy_keys.rb b/qa/qa/page/project/settings/deploy_keys.rb index 467799a14fb..297d29550e3 100644 --- a/qa/qa/page/project/settings/deploy_keys.rb +++ b/qa/qa/page/project/settings/deploy_keys.rb @@ -23,7 +23,7 @@ module QA view 'app/assets/javascripts/deploy_keys/components/key.vue' do element :key_container element :key_title_content - element :key_md5_fingerprint_content + element :key_sha256_fingerprint_content end def add_key @@ -38,17 +38,17 @@ module QA fill_element(:deploy_key_field, key) end - def find_md5_fingerprint(title) + def find_sha256_fingerprint(title) within_project_deploy_keys do find_element(:key_container, text: title) - .find(element_selector_css(:key_md5_fingerprint_content)).text.delete_prefix('MD5:') + .find(element_selector_css(:key_sha256_fingerprint_content)).text end end - def has_key?(title, md5_fingerprint) + def has_key?(title, sha256_fingerprint) within_project_deploy_keys do find_element(:key_container, text: title) - .has_css?(element_selector_css(:key_md5_fingerprint_content), text: "MD5:#{md5_fingerprint}") + .has_css?(element_selector_css(:key_sha256_fingerprint_content), text: sha256_fingerprint) end end diff --git a/qa/qa/page/project/settings/integrations.rb b/qa/qa/page/project/settings/integrations.rb index 420dcb63918..0d5515aacdf 100644 --- a/qa/qa/page/project/settings/integrations.rb +++ b/qa/qa/page/project/settings/integrations.rb @@ -8,12 +8,17 @@ module QA view 'app/assets/javascripts/integrations/index/components/integrations_table.vue' do element :prometheus_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern element :jira_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern + element :pipelines_email_link, %q(:data-qa-selector="`${item.name}_link`") # rubocop:disable QA/ElementWithPattern end def click_on_prometheus_integration click_element :prometheus_link end + def click_pipelines_email_link + click_element :pipelines_email_link + end + def click_jira_link click_element :jira_link end diff --git a/qa/qa/page/project/settings/services/pipeline_status_emails.rb b/qa/qa/page/project/settings/services/pipeline_status_emails.rb new file mode 100644 index 00000000000..2f78577e3d5 --- /dev/null +++ b/qa/qa/page/project/settings/services/pipeline_status_emails.rb @@ -0,0 +1,35 @@ +# frozen_string_literal: true + +module QA + module Page + module Project + module Settings + module Services + class PipelineStatusEmails < QA::Page::Base + view 'app/assets/javascripts/integrations/edit/components/integration_form.vue' do + element :recipients_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern + element :notify_only_broken_pipelines_div, %q(:data-qa-selector="`${field.name}_div`") # rubocop:disable QA/ElementWithPattern + element :save_changes_button + end + + def set_recipients(emails) + within_element :recipients_div do + fill_in 'Recipients', with: emails.join(',') + end + end + + def toggle_notify_broken_pipelines + within_element :notify_only_broken_pipelines_div do + uncheck 'Notify only broken pipelines', allow_label_click: true + end + end + + def click_save_button + click_element(:save_changes_button) + end + end + end + end + end + end +end diff --git a/qa/qa/page/project/show.rb b/qa/qa/page/project/show.rb index 9983ee2a33d..022e08215be 100644 --- a/qa/qa/page/project/show.rb +++ b/qa/qa/page/project/show.rb @@ -44,6 +44,7 @@ module QA end view 'app/views/projects/_files.html.haml' do + element :project_buttons element :tree_holder, '.tree-holder' # rubocop:disable QA/ElementWithPattern end @@ -193,6 +194,12 @@ module QA has_element?(:badge_image_link, link_url: link_url) end end + + def has_license?(name) + within_element(:project_buttons) do + has_link?(name) + 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 3901eff0bcb..bc6c839bfe2 100644 --- a/qa/qa/page/project/web_ide/edit.rb +++ b/qa/qa/page/project/web_ide/edit.rb @@ -192,7 +192,7 @@ module QA find_element(:commit_sha_content).text end - def commit_changes(commit_message = nil, open_merge_request: false) + def commit_changes(commit_message = nil, open_merge_request: false, wait_for_success: true) # Clicking :begin_commit_button switches from the # edit to the commit view click_element(:begin_commit_button) @@ -226,13 +226,17 @@ module QA end click_element(:commit_button) if has_element?(:commit_button) + break unless wait_for_success + # If this is the first commit, the commit SHA only appears after reloading wait_until(reload: true) do active_element?(:edit_mode_tab) && commit_sha != original_commit end end - raise "The changes do not appear to have been committed successfully." unless commit_success + if wait_for_success + raise "The changes do not appear to have been committed successfully." unless commit_success + end end end diff --git a/qa/qa/resource/deploy_key.rb b/qa/qa/resource/deploy_key.rb index c06671be77d..b194f97ef1b 100644 --- a/qa/qa/resource/deploy_key.rb +++ b/qa/qa/resource/deploy_key.rb @@ -7,10 +7,10 @@ module QA attribute :id - attribute :md5_fingerprint do + attribute :sha256_fingerprint do Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |key| - key.find_md5_fingerprint(title) + key.find_sha256_fingerprint(title) end end end diff --git a/qa/qa/resource/design.rb b/qa/qa/resource/design.rb index 182985f2d9f..4a3214f3c0e 100644 --- a/qa/qa/resource/design.rb +++ b/qa/qa/resource/design.rb @@ -3,19 +3,20 @@ module QA module Resource class Design < Base - attr_reader :id - attr_accessor :filename - attribute :issue do Issue.fabricate_via_api! end + attributes :id, + :filename, + :full_path, + :image + def initialize @update = false @filename = 'banana_sample.gif' end - # TODO This will be replaced as soon as file uploads over GraphQL are implemented def fabricate! issue.visit! @@ -24,10 +25,51 @@ module QA end end + def api_get_path + '/graphql' + end + + alias_method :api_post_path, :api_get_path + + # Fetch design + # + # @return [Hash] + def api_get + process_api_response( + api_post_to( + api_get_path, + <<~GQL + query { + issue(id: "gid://gitlab/Issue/#{issue.id}") { + designCollection { + design(filename: "#{filename}") { + id + fullPath + image + filename + } + } + } + } + GQL + ) + ) + end + + # Graphql mutation for design creation + # + # @return [String] + def api_post_body + # TODO: design creation requires file upload via multipart/form-data request type with file passed in mutation + # which currently isn't supported by our api implementation + # https://gitlab.com/gitlab-org/gitlab/-/issues/366592 + raise NotImplementedError, "File uploads are not supported" + end + private def filepath - ::File.absolute_path(::File.join('qa', 'fixtures', 'designs', @filename)) + ::File.absolute_path(::File.join('qa', 'fixtures', 'designs', filename)) end end end diff --git a/qa/qa/resource/group_access_token.rb b/qa/qa/resource/group_access_token.rb new file mode 100644 index 00000000000..934348a1737 --- /dev/null +++ b/qa/qa/resource/group_access_token.rb @@ -0,0 +1,71 @@ +# frozen_string_literal: true + +require 'date' + +module QA + module Resource + class GroupAccessToken < Base + attr_writer :name + + attribute :id + attribute :group do + Group.fabricate! + end + + attribute :token do + Page::Group::Settings::AccessTokens.perform(&:created_access_token) + end + + def api_get_path + "/groups/#{group.id}/access_tokens" + end + + def api_post_path + api_get_path + end + + def name + @name || 'api-group-access-token' + end + + def api_post_body + { + name: name, + scopes: ["api"] + } + end + + def api_delete_path + "/groups/#{group.id}/access_tokens/#{id}" + end + + def resource_web_url(resource) + super + rescue ResourceURLMissingError + # this particular resource does not expose a web_url property + end + + def revoke_via_ui! + Page::Group::Settings::AccessTokens.perform do |tokens_page| + tokens_page.revoke_first_token_with_name(name) + end + end + + def fabricate! + Flow::Login.sign_in_unless_signed_in + + group.visit! + + Page::Group::Menu.perform(&:go_to_access_token_settings) + + Page::Group::Settings::AccessTokens.perform do |token_page| + token_page.fill_token_name(name || 'api-project-access-token') + token_page.check_api + # Expire in 2 days just in case the token is created just before midnight + token_page.fill_expiry_date(Time.now.utc.to_date + 2) + token_page.click_create_token_button + end + end + end + end +end diff --git a/qa/qa/resource/personal_access_token.rb b/qa/qa/resource/personal_access_token.rb index d992d7987b4..a12c210ea0e 100644 --- a/qa/qa/resource/personal_access_token.rb +++ b/qa/qa/resource/personal_access_token.rb @@ -30,7 +30,7 @@ module QA fabricate! end - QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) + QA::Resource::PersonalAccessTokenCache.set_token_for_username(user.username, token) if @user resource end @@ -61,7 +61,7 @@ module QA end def fabricate! - Flow::Login.sign_in_unless_signed_in(as: user) + Flow::Login.sign_in_unless_signed_in(user: user) Page::Main::Menu.perform(&:click_edit_profile_link) Page::Profile::Menu.perform(&:click_access_tokens) diff --git a/qa/qa/resource/personal_access_token_cache.rb b/qa/qa/resource/personal_access_token_cache.rb index 617779173bd..3e9dc3fd7df 100644 --- a/qa/qa/resource/personal_access_token_cache.rb +++ b/qa/qa/resource/personal_access_token_cache.rb @@ -10,6 +10,7 @@ module QA end def self.set_token_for_username(username, token) + QA::Runtime::Logger.info(%Q[Caching token for username: #{username}, last six chars of token:#{token[-6..]}]) @personal_access_tokens[username] = token end end diff --git a/qa/qa/resource/project.rb b/qa/qa/resource/project.rb index 59964c5833d..825041cbead 100644 --- a/qa/qa/resource/project.rb +++ b/qa/qa/resource/project.rb @@ -224,6 +224,10 @@ module QA "#{api_get_path}/releases" end + def api_housekeeping_path + "/projects/#{id}/housekeeping" + end + def api_post_body post_body = { name: name, @@ -447,6 +451,31 @@ module QA end end + # Calls the API endpoint that triggers the backend service that performs repository housekeeping (garbage + # collection and similar tasks). + def perform_housekeeping + Runtime::Logger.debug("Calling API endpoint #{api_housekeeping_path}") + + response = post(request_url(api_housekeeping_path), nil) + + unless response.code == HTTP_STATUS_CREATED + raise ResourceQueryError, + "Could not perform housekeeping. Request returned (#{response.code}): `#{response.body}`." + end + end + + # Gets project statistics. + # + # @return [Hash] the project usage data including repository size. + def statistics + response = get(request_url("#{api_get_path}?statistics=true")) + data = parse_body(response) + + raise "Could not get project usage statistics" unless data.key?(:statistics) + + data[:statistics] + end + protected # Return subset of fields for comparing projects diff --git a/qa/qa/resource/runner.rb b/qa/qa/resource/runner.rb index 278bdd1cabd..6d5ff71b2ba 100644 --- a/qa/qa/resource/runner.rb +++ b/qa/qa/resource/runner.rb @@ -74,12 +74,7 @@ module QA def list_of_runners(tag_list: nil) url = tag_list ? "#{api_post_path}?tag_list=#{tag_list.compact.join(',')}" : api_post_path - response = get(request_url(url, per_page: '100')) - - # Capturing 500 error code responses to log this issue better. We can consider cleaning it up once https://gitlab.com/gitlab-org/gitlab/-/issues/331753 is addressed. - raise "Response returned a #{response.code} error code. #{response.body}" if response.code == Support::API::HTTP_STATUS_SERVER_ERROR - - parse_body(response) + auto_paginated_response(request_url(url)) end def reload! diff --git a/qa/qa/resource/ssh_key.rb b/qa/qa/resource/ssh_key.rb index 9e178a425dd..dd475d7fa66 100644 --- a/qa/qa/resource/ssh_key.rb +++ b/qa/qa/resource/ssh_key.rb @@ -10,7 +10,7 @@ module QA attribute :id - def_delegators :key, :private_key, :public_key, :md5_fingerprint + def_delegators :key, :private_key, :public_key, :md5_fingerprint, :sha256_fingerprint def initialize self.title = Time.now.to_f @@ -39,7 +39,7 @@ module QA end def api_delete - QA::Runtime::Logger.debug("Deleting SSH key with title '#{title}' and fingerprint '#{md5_fingerprint}'") + QA::Runtime::Logger.debug("Deleting SSH key with title '#{title}' and fingerprint '#{sha256_fingerprint}'") super end diff --git a/qa/qa/resource/user.rb b/qa/qa/resource/user.rb index eab428a61e7..2fb8b18b71f 100644 --- a/qa/qa/resource/user.rb +++ b/qa/qa/resource/user.rb @@ -104,12 +104,6 @@ module QA false end - def api_delete - super - - QA::Runtime::Logger.debug("Deleted user '#{username}'") - end - def api_delete_path "/users/#{id}?hard_delete=#{hard_delete_on_api_removal}" rescue NoValueError @@ -163,6 +157,20 @@ module QA end end + # Get users from the API + # + # @param [Integer] per_page the number of pages to traverse (used for pagination) + # @return [Array<Hash>] parsed response body + def self.all(per_page: 100) + response = nil + Resource::User.init do |user| + response = user.get(Runtime::API::Request.new(Runtime::API::Client.as_admin, + '/users', + per_page: per_page.to_s).url) + raise ResourceQueryError unless response.code == 200 + end.parse_body(response) + end + def approve! response = post(Runtime::API::Request.new(api_client, api_approve_path).url, nil) return if response.code == 201 diff --git a/qa/qa/runtime/api/request.rb b/qa/qa/runtime/api/request.rb index c1df5e84f6c..bc1c5fd71ac 100644 --- a/qa/qa/runtime/api/request.rb +++ b/qa/qa/runtime/api/request.rb @@ -46,7 +46,7 @@ module QA if query_string.any? full_path << (path.include?('?') ? '&' : '?') - full_path << query_string.map { |k, v| "#{k}=#{CGI.escape(v)}" }.join('&') + full_path << query_string.map { |k, v| "#{k}=#{CGI.escape(v.to_s)}" }.join('&') end full_path diff --git a/qa/qa/runtime/browser.rb b/qa/qa/runtime/browser.rb index 03178661826..f705fe54b97 100644 --- a/qa/qa/runtime/browser.rb +++ b/qa/qa/runtime/browser.rb @@ -16,7 +16,7 @@ module QA NotRespondingError = Class.new(RuntimeError) - CAPYBARA_MAX_WAIT_TIME = 10 + CAPYBARA_MAX_WAIT_TIME = Env.max_capybara_wait_time def self.blank_page? ['', 'about:blank', 'data:,'].include?(Capybara.current_session.driver.browser.current_url) diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb index 8ad5b107c08..f7aca2571c9 100644 --- a/qa/qa/runtime/env.rb +++ b/qa/qa/runtime/env.rb @@ -263,14 +263,6 @@ module QA ENV['GITLAB_QA_PASSWORD_6'] end - def gitlab_qa_2fa_owner_username_1 - ENV['GITLAB_QA_2FA_OWNER_USERNAME_1'] || 'gitlab-qa-2fa-owner-user1' - end - - def gitlab_qa_2fa_owner_password_1 - ENV['GITLAB_QA_2FA_OWNER_PASSWORD_1'] - end - def gitlab_qa_1p_email ENV['GITLAB_QA_1P_EMAIL'] end @@ -469,6 +461,29 @@ module QA enabled?(ENV['QA_SKIP_SMOKE_RELIABLE'], default: false) end + # ENV variables for authenticating against a private container registry + # These need to be set if using the + # Service::DockerRun::Mixins::ThirdPartyDocker module + def third_party_docker_registry + ENV['QA_THIRD_PARTY_DOCKER_REGISTRY'] + end + + def third_party_docker_repository + ENV['QA_THIRD_PARTY_DOCKER_REPOSITORY'] + end + + def third_party_docker_user + ENV['QA_THIRD_PARTY_DOCKER_USER'] + end + + def third_party_docker_password + ENV['QA_THIRD_PARTY_DOCKER_PASSWORD'] + end + + def max_capybara_wait_time + ENV.fetch('MAX_CAPYBARA_WAIT_TIME', 10).to_i + end + private def remote_grid_credentials diff --git a/qa/qa/runtime/feature.rb b/qa/qa/runtime/feature.rb index db3a59b0549..5e1dd696405 100644 --- a/qa/qa/runtime/feature.rb +++ b/qa/qa/runtime/feature.rb @@ -59,10 +59,15 @@ module QA feature['state'] == 'conditional' && scopes.present? && enabled_scope?(feature['gates'], **scopes) else # The feature wasn't found via the API so we check for a default value. - file = Pathname.new('../config/feature_flags') - .expand_path(Runtime::Path.qa_root) - .glob("**/#{key}.yml") - .first + # We expand the path include both ee and jh. + + pattern = if GitlabEdition.jh? + "#{File.expand_path('../{ee/,jh/,}config/feature_flags', QA::Runtime::Path.qa_root)}/**/#{key}.yml" + else + "#{File.expand_path('../{ee/,}config/feature_flags', QA::Runtime::Path.qa_root)}/**/#{key}.yml" + end + + file = Dir.glob(pattern).first raise UnknownFeatureFlagError, "No feature flag found named '#{key}'" unless file diff --git a/qa/qa/runtime/key/base.rb b/qa/qa/runtime/key/base.rb index 72d1673438a..4ed2ebd3706 100644 --- a/qa/qa/runtime/key/base.rb +++ b/qa/qa/runtime/key/base.rb @@ -4,7 +4,7 @@ module QA module Runtime module Key class Base - attr_reader :name, :bits, :private_key, :public_key, :md5_fingerprint + attr_reader :name, :bits, :private_key, :public_key, :md5_fingerprint, :sha256_fingerprint def initialize(name, bits) @name = name @@ -26,11 +26,15 @@ module QA Service::Shellout.shell(cmd) end + def fingerprint(path, hash_alg) + `ssh-keygen -l -E #{hash_alg} -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp + end + def populate_key_data(path) @private_key = ::File.binread(path) @public_key = ::File.binread("#{path}.pub") - @md5_fingerprint = - `ssh-keygen -l -E md5 -f #{path} | cut -d' ' -f2 | cut -d: -f2-`.chomp + @md5_fingerprint = fingerprint(path, :md5) + @sha256_fingerprint = fingerprint(path, :sha256) end end end diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb index 99906ca44d9..56ab4e14352 100644 --- a/qa/qa/scenario/template.rb +++ b/qa/qa/scenario/template.rb @@ -86,3 +86,5 @@ module QA end end end + +QA::Scenario::Template.prepend_mod_with('Scenario::Template', namespace: QA) diff --git a/qa/qa/service/docker_run/base.rb b/qa/qa/service/docker_run/base.rb index 53980b8e051..c91f68d31a0 100644 --- a/qa/qa/service/docker_run/base.rb +++ b/qa/qa/service/docker_run/base.rb @@ -6,11 +6,34 @@ module QA class Base include Service::Shellout + def self.authenticated_registries + @authenticated_registries ||= {} + end + def initialize @network = Runtime::Scenario.attributes[:network] || 'test' @runner_network = Runtime::Scenario.attributes[:runner_network] || @network end + # Authenticate against a container registry + # If authentication is successful, will cache registry + # + # @param registry [String] registry to authenticate against + # @param user [String] + # @param password [String] + # @param force [Boolean] force authentication if already authenticated + # @return [Void] + def login(registry, user:, password:, force: false) + return if self.class.authenticated_registries[registry] && !force + + shell( + %(docker login --username "#{user}" --password "#{password}" #{registry}), + mask_secrets: [password] + ) + + self.class.authenticated_registries[registry] = true + end + def logs shell "docker logs #{@name}" end diff --git a/qa/qa/service/docker_run/mixins/third_party_docker.rb b/qa/qa/service/docker_run/mixins/third_party_docker.rb new file mode 100644 index 00000000000..b8c66d75d6b --- /dev/null +++ b/qa/qa/service/docker_run/mixins/third_party_docker.rb @@ -0,0 +1,59 @@ +# frozen_string_literal: true + +module QA + module Service + module DockerRun + ThirdPartyValidationError = Class.new(StandardError) + + module Mixins + # Mixin for classes that inherit from Service::DockerRun::Base + # + # Helper for authenticating against private repositories. + # registry.gitlab.com/gitlab-org/quality/third-party-docker-images + module ThirdPartyDocker + # @return [Void] + def authenticate_third_party(force: false) + raise_validation_error unless can_authenticate_third_party? + + login( + third_party_registry, + user: third_party_registry_user, + password: third_party_registry_password, + force: force + ) + end + + def third_party_registry + Runtime::Env.third_party_docker_registry + end + + def third_party_repository + Runtime::Env.third_party_docker_repository + end + + def third_party_registry_user + Runtime::Env.third_party_docker_user + end + + def third_party_registry_password + Runtime::Env.third_party_docker_password + end + + private + + def raise_validation_error + raise ThirdPartyValidationError, 'Third party docker environment variable(s) are not set' + end + + def can_authenticate_third_party? + [ + :third_party_registry, + :third_party_registry_user, + :third_party_registry_password + ].all? { |method| send(method).present? } + end + end + end + end + end +end diff --git a/qa/qa/service/shellout.rb b/qa/qa/service/shellout.rb index 376e4f74845..9b93297f6ff 100644 --- a/qa/qa/service/shellout.rb +++ b/qa/qa/service/shellout.rb @@ -14,7 +14,7 @@ module QA def shell(command, stdin_data: nil, fail_on_exception: true, stream_progress: true, mask_secrets: []) # rubocop:disable Metrics/CyclomaticComplexity cmd_string = Array(command).join(' ') - QA::Runtime::Logger.info("Executing: `#{cmd_string.cyan}`") + QA::Runtime::Logger.info("Executing: `#{mask_secrets_on_string(cmd_string, mask_secrets).cyan}`") Open3.popen2e(*command) do |stdin, out, wait| stdin.puts(stdin_data) if stdin_data diff --git a/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb b/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb new file mode 100644 index 00000000000..7d3916641aa --- /dev/null +++ b/qa/qa/specs/features/api/1_manage/group_access_token_spec.rb @@ -0,0 +1,51 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage' do + describe 'Group access token' do + let(:group_access_token) { QA::Resource::GroupAccessToken.fabricate_via_api! } + let(:api_client) { Runtime::API::Client.new(:gitlab, personal_access_token: group_access_token.token) } + let(:project) do + Resource::Project.fabricate! do |project| + project.name = 'project-for-group-access-token' + project.group = group_access_token.group + project.initialize_with_readme = true + end + end + + it( + 'can be used to create a file via the project API', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367064' + ) do + expect do + Resource::File.fabricate_via_api! do |file| + file.api_client = api_client + file.project = project + file.branch = "new_branch_#{SecureRandom.hex(8)}" + file.commit_message = 'Add new file' + file.name = "text-#{SecureRandom.hex(8)}.txt" + file.content = 'New file' + end + end.not_to raise_error + end + + it( + 'can be used to commit via the API', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367067' + ) do + expect do + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.api_client = 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: "text-#{SecureRandom.hex(8)}.txt", content: 'new file' } + ]) + end + end.not_to raise_error + end + end + end +end diff --git a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb index 6585d08d2ac..c47afbd23f0 100644 --- a/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb +++ b/qa/qa/specs/features/api/1_manage/import_large_github_repo_spec.rb @@ -1,11 +1,10 @@ # frozen_string_literal: true +# Lifesize project import test executed from https://gitlab.com/gitlab-org/manage/import/import-metrics + # rubocop:disable Rails/Pluck module QA - # Only executes in custom job/pipeline - # https://gitlab.com/gitlab-org/manage/import/import-github-performance - # - RSpec.describe 'Manage', :github, :requires_admin, only: { job: 'large-github-import' } do + RSpec.describe 'Manage', :github, requires_admin: 'creates users', only: { job: 'large-github-import' } do describe 'Project import' do let(:logger) { Runtime::Logger.logger } let(:differ) { RSpec::Support::Differ.new(color: true) } @@ -108,7 +107,6 @@ module QA # rubocop:disable RSpec/InstanceVariable after do |example| - user.remove_via_api! unless example.exception next unless defined?(@import_time) # save data for comparison notification creation @@ -117,6 +115,7 @@ module QA { importer: :github, import_time: @import_time, + errors: imported_project.project_import_status[:failed_relations], reported_stats: @stats, source: { name: "GitHub", 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 bb4b0472398..74a00e1c74c 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 @@ -7,27 +7,27 @@ module QA describe 'Gitlab migration' do include_context 'with gitlab project migration' - context 'with project issues' do - let!(:source_issue) do - Resource::Issue.fabricate_via_api! do |issue| - issue.api_client = api_client - issue.project = source_project - issue.labels = %w[label_one label_two] - end + let!(:source_issue) do + Resource::Issue.fabricate_via_api! do |issue| + issue.api_client = api_client + issue.project = source_project + issue.labels = %w[label_one label_two] end + end - let!(:source_comment) { source_issue.add_comment(body: 'This is a test comment!') } - - let(:imported_issues) { imported_projects.first.issues } + let(:imported_issues) { imported_projects.first.issues } - let(:imported_issue) do - issue = imported_issues.first - Resource::Issue.init do |resource| - resource.api_client = api_client - resource.project = imported_projects.first - resource.iid = issue[:iid] - end + let(:imported_issue) do + issue = imported_issues.first + Resource::Issue.init do |resource| + resource.api_client = api_client + resource.project = imported_projects.first + resource.iid = issue[:iid] end + end + + context 'with project issues' do + let!(:source_comment) { source_issue.add_comment(body: 'This is a test comment!') } let(:imported_comments) { imported_issue.comments } @@ -46,6 +46,34 @@ module QA end end end + + context "with designs" do + let!(:source_design) do + Flow::Login.sign_in(as: user) + + Resource::Design.fabricate_via_browser_ui! do |design| + design.api_client = api_client + design.issue = source_issue + end.reload! + end + + let(:imported_design) do + Resource::Design.init do |design| + design.api_client = api_client + design.issue = imported_issue.reload! + end.reload! + end + + it( + 'successfully imports design', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366449' + ) do + expect_import_finished + expect(imported_issues.count).to eq(1) + + expect(imported_design.full_path).to eq(source_design.full_path) + end + end end end end diff --git a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb index 89150c73069..83cc44f9958 100644 --- a/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb +++ b/qa/qa/specs/features/api/1_manage/migration/gitlab_migration_large_project_spec.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true +# Lifesize project import test executed from https://gitlab.com/gitlab-org/manage/import/import-metrics + # rubocop:disable Rails/Pluck, Layout/LineLength, RSpec/MultipleMemoizedHelpers module QA - RSpec.describe "Manage", requires_admin: 'uses admin API client for resource creation', - feature_flag: { name: 'bulk_import_projects', scope: :global }, - only: { job: 'large-gitlab-import' } do + RSpec.describe "Manage", requires_admin: 'creates users', only: { job: 'large-gitlab-import' } do describe "Gitlab migration" do let(:logger) { Runtime::Logger.logger } let(:differ) { RSpec::Support::Differ.new(color: true) } let(:gitlab_group) { ENV['QA_LARGE_IMPORT_GROUP'] || 'gitlab-migration' } let(:gitlab_project) { ENV['QA_LARGE_IMPORT_REPO'] || 'dri' } - let(:gitlab_source_address) { 'https://staging.gitlab.com' } + let(:gitlab_source_address) { ENV['QA_LARGE_IMPORT_SOURCE_URL'] || 'https://staging.gitlab.com' } let(:import_wait_duration) do { @@ -99,8 +99,6 @@ module QA let(:issues) { fetch_issues(imported_project, target_api_client) } before do - Runtime::Feature.enable(:bulk_import_projects) - destination_group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) end @@ -114,6 +112,7 @@ module QA { importer: :gitlab, import_time: @import_time, + errors: imported_group.import_details.sum([]) { |details| details[:failures] }, source: { name: "GitLab Source", project_name: source_project.path_with_namespace, @@ -146,7 +145,7 @@ module QA issue_comments: issues.sum { |_k, v| v[:comments].length } } }, - not_imported: { + diff: { mrs: @mr_diff, issues: @issue_diff } @@ -256,11 +255,12 @@ module QA count_msg = "Expected to contain same amount of #{type}s. Source: #{expected.length}, Target: #{actual.length}" expect(actual.length).to eq(expected.length), count_msg - missing_comments = verify_comments(type, actual, expected) + comment_diff = verify_comments(type, actual, expected) { - "#{type}s": (expected.keys - actual.keys).map { |it| actual[it]&.slice(:title, :url) }.compact, - "#{type}_comments": missing_comments + "missing_#{type}s": (expected.keys - actual.keys).map { |it| actual[it]&.slice(:title, :url) }.compact, + "extra_#{type}s": (actual.keys - expected.keys).map { |it| expected[it]&.slice(:title, :url) }.compact, + "#{type}_comments": comment_diff } end @@ -271,7 +271,7 @@ module QA # @param [Hash] expected # @return [Hash] def verify_comments(type, actual, expected) - actual.each_with_object([]) do |(key, actual_item), missing_comments| + actual.each_with_object([]) do |(key, actual_item), diff| expected_item = expected[key] title = actual_item[:title] msg = "expected #{type} with title '#{title}' to have" @@ -305,16 +305,18 @@ module QA expect(actual_comments.length).to eq(expected_comments.length), comment_count_msg expect(actual_comments).to match_array(expected_comments) - # Save missing comments + # Save comment diff # - comment_diff = expected_comments - actual_comments - next if comment_diff.empty? + missing_comments = expected_comments - actual_comments + extra_comments = actual_comments - expected_comments + next if missing_comments.empty? && extra_comments.empty? - missing_comments << { + diff << { title: title, target_url: actual_item[:url], source_url: expected_item[:url], - missing_comments: comment_diff + missing_comments: missing_comments, + extra_comments: extra_comments } end end @@ -379,10 +381,17 @@ module QA # # @return [Regex] def created_by_pattern - @created_by_pattern ||= /\n\n \*By gitlab-migration on \S+ \(imported from GitLab\)\*/ + @created_by_pattern ||= /\n\n \*By #{importer_username_pattern} on \S+ \(imported from GitLab\)\*/ + end + + # Username of importer user for removal from comments and descriptions + # + # @return [String] + def importer_username_pattern + @importer_username_pattern ||= ENV['QA_LARGE_IMPORT_USER_PATTERN'] || "(gitlab-migration|GitLab QA Bot)" end - # Remove added prefixes and legacy diff format from comments + # Remove added prefixes from comments # # @param [String] body # @return [String] 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 5a9cef6fded..3581ad3d207 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 @@ -68,7 +68,7 @@ module QA imported_group # trigger import expect { imported_group.import_status }.to eventually_eq('finished').within(import_wait_duration) - expect(imported_projects.count).to eq(1), 'Expected to have 1 imported project' + expect(imported_projects.count).to eq(1), "Expected to have 1 imported project. Found: #{imported_projects.count}" end before do diff --git a/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb b/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb new file mode 100644 index 00000000000..406ff191f95 --- /dev/null +++ b/qa/qa/specs/features/api/3_create/repository/storage_size_spec.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Repository Usage Quota', :skip_live_env, feature_flag: { + name: 'gitaly_revlist_for_repo_size', + scope: :global + } do + let(:project_name) { "repository-usage-#{SecureRandom.hex(8)}" } + let!(:flag_enabled) { Runtime::Feature.enabled?(:gitaly_revlist_for_repo_size) } + + before do + Runtime::Feature.enable(:gitaly_revlist_for_repo_size) + end + + after do + Runtime::Feature.set({ gitaly_revlist_for_repo_size: flag_enabled }) + end + + # Previously, GitLab could report a size many times larger than a cloned copy. For example, 37Gb reported for a + # repo that is 2Gb when cloned. + # + # After changing Gitaly to use `git rev-list` to determine the size of a repo, the reported size is much more + # accurate. Nonetheless, the size of a clone is still not necessarily the same as the original. We can't do a + # precise comparison because of the non-deterministic nature of how git packs files. Depending on the history of + # the repository the sizes can vary considerably. For example, at the time of writing this a clone of + # www-gitlab-com was 5.27Gb, about 5% smaller than the size GitLab reported, 5.51Gb. + # + # There are unit tests to verify the accuracy of GitLab's determination of repo size, so for this test we + # attempt to detect large differences that could indicate a regression to previous behavior. + it 'matches cloned repo usage to reported usage', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/365196' do + project = Resource::Project.fabricate_via_api! do |project| + project.name = project_name + end + + shared_data = SecureRandom.random_bytes(500000) + + Resource::Repository::ProjectPush.fabricate! do |push| + push.project = project + push.file_name = 'data.dat' + push.file_content = SecureRandom.random_bytes(500000) + shared_data + push.commit_message = 'Add file' + end + + local_size = Git::Repository.perform do |repository| + repository.uri = project.repository_http_location.uri + repository.use_default_credentials + repository.clone + repository.configure_identity('GitLab QA', 'root@gitlab.com') + # These two commits add a total of 1mb, but half of that is the same as content that has already been added to + # the repository, so garbage collection will deduplicate it. + repository.commit_file("new-data", SecureRandom.random_bytes(500000), "Add file") + repository.commit_file("redudant-data", shared_data, "Add file") + repository.run_gc + repository.push_changes + repository.local_size + end + + # The size of the remote repository after all content has been added. + initial_size = project.statistics[:repository_size].to_i + + # This is an async process and as a user we have no way to know when it's complete unless the statistics are + # updated + Support::Retrier.retry_until(max_duration: 60, sleep_interval: 5) do + # This should perform the same deduplication as in the local repo + project.perform_housekeeping + + project.statistics[:repository_size].to_i != initial_size + end + + twentyfive_percent = local_size.to_i * 0.25 + expect(project.statistics[:repository_size].to_i).to be_within(twentyfive_percent).of(local_size) + end + end + end +end diff --git a/qa/qa/specs/features/api/5_package/container_registry_spec.rb b/qa/qa/specs/features/api/5_package/container_registry_spec.rb index d7207803d45..79384e374d7 100644 --- a/qa/qa/specs/features/api/5_package/container_registry_spec.rb +++ b/qa/qa/specs/features/api/5_package/container_registry_spec.rb @@ -3,8 +3,9 @@ require 'airborne' module QA - RSpec.describe 'Package', only: { subdomain: %i[staging pre] }, quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/360466', type: :investigating } do + RSpec.describe 'Package', only: { subdomain: %i[staging pre] } do include Support::API + include Support::Helpers::MaskToken describe 'Container Registry' do let(:api_client) { Runtime::API::Client.new(:gitlab) } @@ -17,6 +18,12 @@ module QA end end + let!(:project_access_token) do + QA::Resource::ProjectAccessToken.fabricate_via_api! do |pat| + pat.project = project + end + end + let(:registry) do Resource::RegistryRepository.init do |repository| repository.name = project.path_with_namespace @@ -25,45 +32,47 @@ module QA end end + let(:masked_token) do + use_ci_variable(name: 'PAT', value: project_access_token.token, project: project) + end + let(:gitlab_ci_yaml) do <<~YAML - stages: - - build - - test - - build: - image: docker:19.03.12 - stage: build - services: - - docker:19.03.12-dind - variables: - IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG - DOCKER_HOST: tcp://docker:2376 - DOCKER_TLS_CERTDIR: "/certs" - DOCKER_TLS_VERIFY: 1 - DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client" - before_script: - - until docker info; do sleep 1; done - script: - - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY - - docker build -t $IMAGE_TAG . - - docker push $IMAGE_TAG - - docker pull $IMAGE_TAG - - test: - image: dwdraju/alpine-curl-jq:latest - stage: test - variables: - MEDIA_TYPE: 'application/vnd.docker.distribution.manifest.v2+json' - before_script: - - token=$(curl -u "$CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD" "https://$CI_SERVER_HOST/jwt/auth?service=container_registry&scope=repository:$CI_PROJECT_PATH:pull,push,delete" | jq -r '.token') - script: - - 'digest=$(curl -L -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/master" | jq -r ".layers[0].digest")' - - 'curl -L -X DELETE -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/blobs/$digest"' - - 'curl -L --head -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/blobs/$digest"' - - 'digest=$(curl -L -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/master" | jq -r ".config.digest")' - - 'curl -L -X DELETE -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/$digest"' - - 'curl -L --head -H "Authorization: Bearer $token" -H "Accept: $MEDIA_TYPE" "https://$CI_REGISTRY/v2/$CI_PROJECT_PATH/manifests/$digest"' + stages: + - build + - test + + build: + image: docker:19.03.12 + stage: build + services: + - docker:19.03.12-dind + variables: + IMAGE_TAG: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG + DOCKER_HOST: tcp://docker:2376 + DOCKER_TLS_CERTDIR: "/certs" + DOCKER_TLS_VERIFY: 1 + DOCKER_CERT_PATH: "$DOCKER_TLS_CERTDIR/client" + before_script: + - until docker info; do sleep 1; done + script: + - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY + - docker build -t $IMAGE_TAG . + - docker push $IMAGE_TAG + - docker pull $IMAGE_TAG + + test: + image: dwdraju/alpine-curl-jq:latest + stage: test + script: + - 'id=$(curl --header "PRIVATE-TOKEN: #{masked_token}" "https://${CI_SERVER_HOST}/api/v4/projects/#{project.id}/registry/repositories" | jq ".[0].id")' + - echo $id + - 'tag_count=$(curl --header "PRIVATE-TOKEN: #{masked_token}" "https://${CI_SERVER_HOST}/api/v4/projects/#{project.id}/registry/repositories/$id/tags" | jq ". | length")' + - if [ $tag_count -ne 1 ]; then exit 1; fi; + - 'status_code=$(curl --request DELETE --head --output /dev/null --write-out "%{http_code}\n" --header "PRIVATE-TOKEN: #{masked_token}" "https://${CI_SERVER_HOST}/api/v4/projects/#{project.id}/registry/repositories/$id/tags/master")' + - if [ $status_code -ne 200 ]; then exit 1; fi; + - 'status_code=$(curl --head --output /dev/null --write-out "%{http_code}\n" --header "PRIVATE-TOKEN: #{masked_token}" "https://${CI_SERVER_HOST}/api/v4/projects/#{project.id}/registry/repositories/$id/tags/master")' + - if [ $status_code -ne 404 ]; then exit 1; fi; YAML end @@ -71,7 +80,7 @@ module QA registry&.remove_via_api! end - it 'pushes, pulls image to the registry and deletes image blob, manifest and tag', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348001' do + it 'pushes, pulls image to the registry and deletes tag', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348001' do Support::Retrier.retry_on_exception(max_attempts: 3, sleep_interval: 2) do Resource::Repository::Commit.fabricate_via_api! do |commit| commit.api_client = api_client @@ -89,14 +98,6 @@ module QA Support::Retrier.retry_until(max_duration: 300, sleep_interval: 5) do latest_pipeline_succeed? end - - expect(job_log).to have_content '404 Not Found' - - expect(registry).to have_tag('master') - - registry.delete_tag - - expect(registry).not_to have_tag('master') end private @@ -109,20 +110,6 @@ module QA latest_pipeline = project.pipelines.first latest_pipeline[:status] == 'success' end - - def job_log - pipeline = project.pipelines.first - pipeline_id = pipeline[:id] - - jobs = get Runtime::API::Request.new(api_client, "/projects/#{project.id}/pipelines/#{pipeline_id}/jobs").url - test_job = parse_body(jobs).first - test_job_id = test_job[:id] - - log = get Runtime::API::Request.new(api_client, "/projects/#{project.id}/jobs/#{test_job_id}/trace").url - QA::Runtime::Logger.debug(" \n\n ------- Test job log: ------- \n\n #{log} \n -------") - - log - end end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb new file mode 100644 index 00000000000..9f39d376baf --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/group/group_access_token_spec.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage' do + describe 'Group access tokens' do + let(:group_access_token) { QA::Resource::GroupAccessToken.fabricate_via_browser_ui! } + + it( + 'can be created and revoked via the UI', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/367044' + ) do + expect(group_access_token.token).not_to be_nil + + group_access_token.revoke_via_ui! + expect(page).to have_text("Revoked access token #{group_access_token.name}!") + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb index c86a649f179..f459c0c71eb 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/2fa_recovery_spec.rb @@ -1,10 +1,12 @@ # frozen_string_literal: true module QA - RSpec.describe 'Manage', :requires_admin, :skip_live_env do + RSpec.describe 'Manage', :requires_admin, :skip_live_env, :reliable do describe '2FA' do let(:owner_user) do - Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_2fa_owner_username_1, Runtime::Env.gitlab_qa_2fa_owner_password_1) + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + end end let(:developer_user) do @@ -32,7 +34,10 @@ module QA group.add_member(developer_user, Resource::Members::AccessLevel::DEVELOPER) end - it 'allows using 2FA recovery code once only', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347937' do + it( + 'allows using 2FA recovery code once only', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347937' + ) do recovery_code = enable_2fa_for_user_and_fetch_recovery_code(developer_user) Flow::Login.sign_in(as: developer_user, skip_page_validation: true) @@ -56,13 +61,6 @@ module QA expect(page).to have_text('Invalid two-factor code') end - after do - group.set_require_two_factor_authentication(value: 'false') - group.remove_via_api! - sandbox_group.remove_via_api! - developer_user.remove_via_api! - end - def admin_api_client @admin_api_client ||= Runtime::API::Client.as_admin end @@ -74,9 +72,9 @@ module QA def enable_2fa_for_user_and_fetch_recovery_code(user) Flow::Login.while_signed_in(as: user) do Page::Profile::TwoFactorAuth.perform do |two_fa_auth| - @otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content) + otp = QA::Support::OTP.new(two_fa_auth.otp_secret_content) - two_fa_auth.set_pin_code(@otp.fresh_otp) + two_fa_auth.set_pin_code(otp.fresh_otp) two_fa_auth.set_current_password(user.password) two_fa_auth.click_register_2fa_app_button diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb index 64614ed654f..c5efa833f04 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/login/log_in_with_2fa_spec.rb @@ -3,13 +3,15 @@ module QA RSpec.describe 'Manage', :requires_admin, :skip_live_env do describe '2FA' do - let(:owner_user) do - Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_2fa_owner_username_1, Runtime::Env.gitlab_qa_2fa_owner_password_1) + let!(:owner_user) do + Resource::User.fabricate_via_api! do |usr| + usr.api_client = admin_api_client + end end let(:sandbox_group) do Resource::Sandbox.fabricate! do |sandbox_group| - sandbox_group.path = "gitlab-qa-2fa-sandbox-group" + sandbox_group.path = "gitlab-qa-2fa-sandbox-group-#{SecureRandom.hex(8)}" sandbox_group.api_client = owner_api_client end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/register_spec.rb index 5487ecff028..85ab466078a 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 @@ -86,7 +86,10 @@ module QA end after do - @recreated_user&.remove_via_api! + if @recreated_user + @recreated_user.api_client = admin_api_client + @recreated_user.remove_via_api! + end end def admin_api_client diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb index 44cae31f5d8..b1d59b90e9c 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/dashboard_images_spec.rb @@ -2,49 +2,45 @@ module QA RSpec.describe 'Manage' do - describe 'Check for broken images', :requires_admin do - before(:context) do - @api_client = Runtime::API::Client.as_admin - @new_user = Resource::User.fabricate_via_api! do |user| - user.api_client = @api_client - end - @new_admin = Resource::User.fabricate_via_api! do |user| - user.admin = true - user.api_client = @api_client - end + shared_examples 'loads all images' do |admin| + let(:api_client) { Runtime::API::Client.as_admin } - Page::Main::Menu.perform(&:sign_out_if_signed_in) + let(:user) do + Resource::User.fabricate_via_api! do |resource| + resource.admin = admin + resource.api_client = api_client + end end - after(:context) do - @new_user.remove_via_api! - @new_admin.remove_via_api! + after do + user.remove_via_api! end - shared_examples 'loads all images' do - it 'loads all images' do - Runtime::Browser.visit(:gitlab, Page::Main::Login) - Page::Main::Login.perform { |login| login.sign_in_using_credentials(user: new_user) } + it 'loads all images' do + Flow::Login.sign_in(as: user) - Page::Dashboard::Welcome.perform do |welcome| - expect(welcome).to have_welcome_title("Welcome to GitLab") + Page::Dashboard::Welcome.perform do |welcome| + expect(welcome).to have_welcome_title("Welcome to GitLab") - # This would be better if it were a visual validation test - expect(welcome).to have_loaded_all_images - end + # This would be better if it were a visual validation test + expect(welcome).to have_loaded_all_images end end + end - context 'when logged in as a new user', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347885' do - it_behaves_like 'loads all images' do - let(:new_user) { @new_user } - end + describe 'Check for broken images', :requires_admin, :reliable do + context( + 'when logged in as a new user', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347885' + ) do + it_behaves_like 'loads all images', false end - context 'when logged in as a new admin', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347884' do - it_behaves_like 'loads all images' do - let(:new_user) { @new_admin } - end + context( + 'when logged in as a new admin', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347884' + ) do + it_behaves_like 'loads all images', true end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb index dd27e85af3c..8201e2772aa 100644 --- a/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb +++ b/qa/qa/specs/features/browser_ui/1_manage/project/invite_group_to_project_spec.rb @@ -1,8 +1,7 @@ # frozen_string_literal: true module QA - # Tagging with issue for a transient invite group modal search bug, but does not require quarantine at this time - RSpec.describe 'Manage', :transient, issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/349379' do + RSpec.describe 'Manage' do describe 'Invite group' do shared_examples 'invites group to project' do it 'verifies group is added and members can access project with correct access level' do @@ -13,7 +12,7 @@ module QA expect(project_members).to have_group(group.path) end - Flow::Login.sign_in(as: @user) + Flow::Login.sign_in(as: user) Page::Dashboard::Projects.perform do |projects| projects.filter_by_name(project.name) @@ -29,13 +28,11 @@ module QA end end - before(:context) do - @user = Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) - end + let(:user) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } before do Flow::Login.sign_in - group.add_member(@user, Resource::Members::AccessLevel::MAINTAINER) + group.add_member(user, Resource::Members::AccessLevel::MAINTAINER) project.visit! end @@ -75,11 +72,6 @@ module QA it_behaves_like 'invites group to project' end - - after do - project&.remove_via_api! - group&.remove_via_api! - end end end end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb deleted file mode 100644 index fb486ab1532..00000000000 --- a/qa/qa/specs/features/browser_ui/1_manage/project/personal_project_permissions_spec.rb +++ /dev/null @@ -1,94 +0,0 @@ -# frozen_string_literal: true - -module QA - RSpec.describe 'Manage' do - describe 'Personal project permissions', :reliable do - let!(:owner) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } - - let!(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner) } - - let!(:project) do - Resource::Project.fabricate_via_api! do |project| - project.api_client = owner_api_client - project.name = 'qa-owner-personal-project' - project.personal_namespace = owner.username - end - end - - after do - project&.remove_via_api! - end - - context 'when user is added as Owner' do - let(:issue) do - Resource::Issue.fabricate_via_api! do |issue| - issue.api_client = owner_api_client - issue.project = project - issue.title = 'Test Owner deletes issue' - end - end - - before do - Flow::Login.sign_in(as: owner) - end - - it "has Owner role with Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352542' do - Page::Dashboard::Projects.perform do |projects| - projects.filter_by_name(project.name) - - expect(projects).to have_project_with_access_role(project.name, 'Owner') - end - - expect_owner_permissions_allow_delete_issue - end - end - - context 'when user is added as Maintainer' do - let(:maintainer) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) } - - let(:issue) do - Resource::Issue.fabricate_via_api! do |issue| - issue.api_client = owner_api_client - issue.project = project - issue.title = 'Test Maintainer deletes issue' - end - end - - before do - project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER) - Flow::Login.sign_in(as: maintainer) - end - - it "has Maintainer role without Owner permissions", testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352607' do - Page::Dashboard::Projects.perform do |projects| - projects.filter_by_name(project.name) - - expect(projects).to have_project_with_access_role(project.name, 'Maintainer') - end - - expect_maintainer_permissions_do_not_allow_delete_issue - end - end - - private - - def expect_owner_permissions_allow_delete_issue - issue.visit! - - Page::Project::Issue::Show.perform(&:delete_issue) - - Page::Project::Issue::Index.perform do |index| - expect(index).not_to have_issue(issue) - end - end - - def expect_maintainer_permissions_do_not_allow_delete_issue - issue.visit! - - Page::Project::Issue::Show.perform do |issue| - expect(issue).not_to have_delete_issue_button - end - end - end - end -end diff --git a/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb new file mode 100644 index 00000000000..2f148c4051c --- /dev/null +++ b/qa/qa/specs/features/browser_ui/1_manage/project/project_owner_permissions_spec.rb @@ -0,0 +1,104 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Manage' do + describe 'Project owner permissions' do + let!(:owner) do + Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) + end + + let!(:owner_api_client) { Runtime::API::Client.new(:gitlab, user: owner) } + + let!(:maintainer) do + Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) + end + + shared_examples 'when user is added as owner' do |project_type, testcase| + let!(:issue) do + Resource::Issue.fabricate_via_api! do |issue| + issue.api_client = owner_api_client + issue.project = project + issue.title = 'Test Owner Deletes Issue' + end + end + + before do + project.add_member(owner, Resource::Members::AccessLevel::OWNER) if project_type == :group_project + Flow::Login.sign_in(as: owner) + end + + it "has owner role with owner permissions", testcase: testcase do + Page::Dashboard::Projects.perform do |projects| + projects.filter_by_name(project.name) + + expect(projects).to have_project_with_access_role(project.name, 'Owner') + end + + issue.visit! + + Page::Project::Issue::Show.perform(&:delete_issue) + + Page::Project::Issue::Index.perform do |index| + expect(index).not_to have_issue(issue) + end + end + end + + shared_examples 'when user is added as maintainer' do |testcase| + let!(:issue) do + Resource::Issue.fabricate_via_api! do |issue| + issue.api_client = owner_api_client + issue.project = project + issue.title = 'Test Maintainer Deletes Issue' + end + end + + before do + project.add_member(maintainer, Resource::Members::AccessLevel::MAINTAINER) + Flow::Login.sign_in(as: maintainer) + end + + it "has maintainer role without owner permissions", testcase: testcase do + Page::Dashboard::Projects.perform do |projects| + projects.filter_by_name(project.name) + + expect(projects).to have_project_with_access_role(project.name, 'Maintainer') + end + + issue.visit! + + Page::Project::Issue::Show.perform do |issue| + expect(issue).not_to have_delete_issue_button + end + end + end + + context 'for personal projects' do + let!(:project) do + Resource::Project.fabricate_via_api! do |project| + project.api_client = owner_api_client + project.name = 'qa-owner-personal-project' + project.personal_namespace = owner.username + end + end + + it_behaves_like 'when user is added as owner', :personal_project, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352542' + it_behaves_like 'when user is added as maintainer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/352607' + end + + context 'for group projects' do + let!(:group) { Resource::Group.fabricate_via_api! } + + let!(:project) do + Resource::Project.fabricate_via_api! do |project| + project.group = group + project.name = 'qa-owner-group-project' + end + end + + it_behaves_like 'when user is added as owner', :group_project, 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366436' + it_behaves_like 'when user is added as maintainer', 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366435' + end + end + end +end 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 11cf4f60a80..a384dc16064 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 @@ -24,6 +24,7 @@ module QA let(:group) do group = QA::Resource::Group.fabricate_via_api! do |group| group.path = "group_for_follow_user_activity_#{SecureRandom.hex(8)}" + group.api_client = admin_api_client end group.add_member(followed_user, Resource::Members::AccessLevel::MAINTAINER) group diff --git a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb index f41e5985622..b815186cd49 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/email/trigger_email_notification_spec.rb @@ -46,19 +46,22 @@ module QA total = mailhog_data.dig('total') subjects = mailhog_data.dig('items') .map(&method(:mailhog_item_subject)) - .join("\n") Runtime::Logger.debug(%Q[Total number of emails: #{total}]) - Runtime::Logger.debug(%Q[Subjects:\n#{subjects}]) + Runtime::Logger.debug(%Q[Subjects:\n#{subjects.join("\n")}]) # Expect at least two invitation messages: group and project - mailhog_data if total >= 2 + mailhog_data if mailhog_project_message_count(subjects) >= 1 end end def mailhog_item_subject(item) item.dig('Content', 'Headers', 'Subject', 0) end + + def mailhog_project_message_count(subjects) + subjects.count { |subject| subject.include?('project was granted') } + end end end end diff --git a/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb b/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb index 9c90cf6ae3d..66e2309e173 100644 --- a/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb +++ b/qa/qa/specs/features/browser_ui/2_plan/issue/real_time_assignee_spec.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module QA - RSpec.describe 'Plan', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/293699', type: :bug } do + RSpec.describe 'Plan' do describe 'Assignees' do let(:user1) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_1, Runtime::Env.gitlab_qa_password_1) } let(:user2) { Resource::User.fabricate_or_use(Runtime::Env.gitlab_qa_username_2, Runtime::Env.gitlab_qa_password_2) } 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..71415c4bb57 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 @@ -1,7 +1,11 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', quarantine: { + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/366839', + type: :test_environment, + only: { job: 'review-qa-*' } + } do context 'Design Management' do let(:issue) { Resource::Issue.fabricate_via_api! } let(:design_filename) { 'banana_sample.gif' } diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb index de7c95841d6..9b969f563a2 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/archive_design_content_spec.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', quarantine: { + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/366839', + type: :test_environment, + only: { job: 'review-qa-*' } + } do context 'Design Management' do let(:first_design) { Resource::Design.fabricate! } @@ -13,7 +17,7 @@ module QA end let(:third_design) do - Resource::Design.fabricate! do |design| + Resource::Design.fabricate_via_browser_ui! do |design| design.issue = second_design.issue design.filename = 'testfile.png' end diff --git a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb index 726e86ccdb4..6a0c51245ad 100644 --- a/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/design_management/modify_design_content_spec.rb @@ -1,10 +1,14 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', quarantine: { + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/366839', + type: :test_environment, + only: { job: 'review-qa-*' } + } do context 'Design Management' do let(:design) do - Resource::Design.fabricate! do |design| + Resource::Design.fabricate_via_browser_ui! do |design| design.filename = 'testfile.png' end end @@ -13,7 +17,10 @@ module QA Flow::Login.sign_in end - it 'user adds a design and modifies it', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347712' do + it( + 'user adds a design and modifies it', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347712' + ) do design.issue.visit! Page::Project::Issue::Show.perform do |issue| diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb index f4ed9f28dac..25dec82b74c 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/create_merge_request_spec.rb @@ -24,6 +24,7 @@ module QA Resource::MergeRequest.fabricate_via_browser_ui! do |merge_request| merge_request.project = project merge_request.title = merge_request_title + merge_request.assignee = 'me' merge_request.description = merge_request_description end @@ -53,6 +54,7 @@ module QA merge_request.description = merge_request_description merge_request.project = project merge_request.milestone = milestone + merge_request.assignee = 'me' merge_request.labels.push(label) end diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb index d66895de9c1..948aedf5aae 100644 --- a/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/revert/reverting_merge_request_spec.rb @@ -9,7 +9,7 @@ module QA end end - let(:revertable_merge_request) do + let(:revertible_merge_request) do Resource::MergeRequest.fabricate_via_api! do |merge_request| merge_request.project = project end @@ -20,10 +20,11 @@ module QA end it 'can be reverted', :can_use_large_setup, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347709' do - revertable_merge_request.visit! + revertible_merge_request.visit! Page::MergeRequest::Show.perform do |merge_request| merge_request.merge! + expect(merge_request).to be_revertible, 'Expected merge request to be in a state to be reverted.' merge_request.revert_change! end @@ -31,7 +32,7 @@ module QA Page::MergeRequest::Show.perform do |merge_request| merge_request.click_diffs_tab - expect(merge_request).to have_file(revertable_merge_request.file_name) + expect(merge_request).to have_file(revertible_merge_request.file_name) end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/license_detecton_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/license_detecton_spec.rb new file mode 100644 index 00000000000..8074e1fa992 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/repository/license_detecton_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create' do + describe 'Repository License Detection' do + after do + project.remove_via_api! + end + + let(:project) { Resource::Project.fabricate_via_api! } + + shared_examples 'project license detection' do + it 'displays the name of the license on the repository' do + license_path = File.expand_path("../../../../../fixtures/software_licenses/#{license_file_name}", __dir__) + Resource::Repository::Commit.fabricate_via_api! do |commit| + commit.project = project + commit.add_files([{ file_path: 'LICENSE', content: File.read(license_path) }]) + end + + project.visit! + + Page::Project::Show.perform do |show| + expect(show).to have_license(rendered_license_name) + end + end + end + + context 'on a project with a commonly used LICENSE', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366842' do + it_behaves_like 'project license detection' do + let(:license_file_name) {'bsd-3-clause'} + let(:rendered_license_name) {'BSD 3-Clause "New" or "Revised" License'} + end + end + + context 'on a project with a less commonly used LICENSE', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366843' do + it_behaves_like 'project license detection' do + let(:license_file_name) {'GFDL-1.2-only'} + let(:rendered_license_name) {'Other'} + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb index 4228c3ed352..9ab322df824 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_mirroring_lfs_over_http_spec.rb @@ -1,7 +1,11 @@ # frozen_string_literal: true module QA - RSpec.describe 'Create' do + RSpec.describe 'Create', quarantine: { + issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/352525', + type: :test_environment, + only: { job: 'review-qa-*' } + } do describe 'Push mirror a repository over HTTP' do it 'configures and syncs LFS objects for a (push) mirrored repository', :aggregate_failures, testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/347847' do Runtime::Browser.visit(:gitlab, Page::Main::Login) diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb index 18a77bd5ae3..0323448878b 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/push_over_ssh_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'SSH key support', :skip_fips_env do + describe 'SSH key support' do # Note: If you run these tests against GDK make sure you've enabled sshd # See: https://gitlab.com/gitlab-org/gitlab-qa/blob/master/docs/run_qa_against_gdk.md diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb index a1a795ce0f3..f374ecff3f2 100644 --- a/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/repository/ssh_key_support_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'SSH keys support', :smoke, :skip_fips_env do + describe 'SSH keys support', :smoke do key_title = "key for ssh tests #{Time.now.to_f}" key = nil @@ -16,7 +16,7 @@ module QA end expect(page).to have_content(key.title) - expect(page).to have_content(key.md5_fingerprint) + expect(page).to have_content(key.sha256_fingerprint) end # Note this context ensures that the example it contains is executed after the example above. Be aware of the order of execution if you add new examples in either context. @@ -29,7 +29,7 @@ module QA end expect(page).not_to have_content("Title: #{key.title}") - expect(page).not_to have_content(key.md5_fingerprint) + expect(page).not_to have_content(key.sha256_fingerprint) end end end diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb index 7a0b4674581..8f22a28628f 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_personal_snippet_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Version control for personal snippets', :skip_fips_env do + describe 'Version control for personal snippets' do let(:new_file) { 'new_snippet_file' } let(:changed_content) { 'changes' } let(:commit_message) { 'Changes to snippets' } diff --git a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb index d269e02e26d..9a5fe44c927 100644 --- a/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb +++ b/qa/qa/specs/features/browser_ui/3_create/snippet/clone_push_pull_project_snippet_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Create' do - describe 'Version control for project snippets', :skip_fips_env do + describe 'Version control for project snippets' do let(:new_file) { 'new_snippet_file' } let(:changed_content) { 'changes' } let(:commit_message) { 'Changes to snippets' } diff --git a/qa/qa/specs/features/browser_ui/3_create/web_ide/server_hooks_custom_error_message_spec.rb b/qa/qa/specs/features/browser_ui/3_create/web_ide/server_hooks_custom_error_message_spec.rb new file mode 100644 index 00000000000..ac2d374b0ed --- /dev/null +++ b/qa/qa/specs/features/browser_ui/3_create/web_ide/server_hooks_custom_error_message_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +module QA + RSpec.describe 'Create', :skip_live_env, except: { job: 'review-qa-*' } do + describe 'Git Server Hooks' do + let(:file_path) { File.absolute_path(File.join('qa', 'fixtures', 'web_ide', 'README.md')) } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + # Projects that have names that include pattern 'reject-prereceive' trigger a server hook on orchestrated env + # that returns an error string using GL-HOOK-ERR + project.name = "project-reject-prereceive-#{SecureRandom.hex(8)}" + end + end + + before do + Flow::Login.sign_in + project.visit! + end + + context 'Custom error messages' do + it 'renders preconfigured error message when user hook failed on commit in WebIDE', + testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/364751' do + Page::Project::Show.perform(&:open_web_ide_via_shortcut) + Page::Project::WebIDE::Edit.perform do |ide| + ide.upload_file(file_path) + ide.commit_changes(wait_for_success: false) + expect(ide).to have_text('Custom error message rejecting prereceive hook for projects with GL_PROJECT_PATH') + end + end + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb index 23f212e110b..02ee94381b2 100644 --- a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_editor_lint_spec.rb @@ -2,7 +2,12 @@ module QA RSpec.describe 'Verify' do - describe 'Pipeline editor', :reliable do + # TODO: Remove this test when feature flag is removed + # Flag rollout issue https://gitlab.com/gitlab-org/gitlab/-/issues/364257 + describe 'Pipeline editor', :reliable, feature_flag: { + name: :simulate_pipeline, + scope: :global + } do let(:project) do Resource::Project.fabricate_via_api! do |project| project.name = 'pipeline-editor-project' @@ -37,6 +42,8 @@ module QA end before do + Runtime::Feature.disable(:simulate_pipeline) if Runtime::Feature.enabled?(:simulate_pipeline) + Flow::Login.sign_in project.visit! Page::Project::Menu.perform(&:go_to_pipeline_editor) @@ -50,7 +57,7 @@ module QA it 'shows valid validations', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/349128' do Page::Project::PipelineEditor::Show.perform do |show| aggregate_failures do - expect(show.ci_syntax_validate_message).to have_content('CI configuration is valid') + expect(show.ci_syntax_validate_message).to have_content('Pipeline syntax is correct') show.go_to_visualize_tab { stage1: 'job1', stage2: 'job2' }.each_pair do |stage, job| diff --git a/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb new file mode 100644 index 00000000000..f4794b3a904 --- /dev/null +++ b/qa/qa/specs/features/browser_ui/4_verify/pipeline/pipeline_status_emails_spec.rb @@ -0,0 +1,134 @@ +# frozen_string_literal: true + +module QA + RSpec.shared_examples 'notifies on a pipeline' do |exit_code| + before do + push_commit(exit_code: exit_code) + end + + it 'sends an email' do + meta = exit_code_meta(exit_code) + + project.visit! + Flow::Pipeline.wait_for_latest_pipeline(status: meta[:status]) + + messages = mail_hog_messages(mail_hog) + subjects = messages.map(&:subject) + targets = messages.map(&:to) + + aggregate_failures do + expect(subjects).to include(meta[:email_subject]) + expect(subjects).to include(/#{Regexp.escape(project.name)}/) + expect(targets).to include(*emails) + end + end + end + + RSpec.describe 'Verify', :orchestrated, :runner, :requires_admin, :smtp do + describe 'Pipeline status emails' do + let(:executor) { "qa-runner-#{Time.now.to_i}" } + let(:emails) { %w[foo@bar.com baz@buzz.com] } + + let(:project) do + Resource::Project.fabricate_via_api! do |project| + project.name = 'pipeline-status-project' + end + end + + let!(:runner) do + Resource::Runner.fabricate! do |runner| + runner.project = project + runner.name = executor + runner.tags = [executor] + end + end + + let(:mail_hog) { Vendor::MailHog::API.new } + + before(:all) do + Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true) + end + + before do + setup_pipeline_emails(emails) + end + + describe 'when pipeline passes', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366240' do + include_examples 'notifies on a pipeline', 0 + end + + describe 'when pipeline fails', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/366241' do + include_examples 'notifies on a pipeline', 1 + end + + def push_commit(exit_code: 0) + 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: gitlab_ci_yaml(exit_code: exit_code) + } + ] + ) + end + end + + def setup_pipeline_emails(emails) + page.visit Runtime::Scenario.gitlab_address + Flow::Login.sign_in_unless_signed_in + + project.visit! + + Page::Project::Menu.perform(&:go_to_integrations_settings) + QA::Page::Project::Settings::Integrations.perform(&:click_pipelines_email_link) + + QA::Page::Project::Settings::Services::PipelineStatusEmails.perform do |pipeline_status_emails| + pipeline_status_emails.toggle_notify_broken_pipelines # notify on pass and fail + pipeline_status_emails.set_recipients(emails) + pipeline_status_emails.click_save_button + end + end + + def gitlab_ci_yaml(exit_code: 0, tag: executor) + <<~YAML + test-pipeline-email: + tags: + - #{tag} + script: sleep 5; exit #{exit_code}; + YAML + end + + private + + def exit_code_meta(exit_code) + { + 0 => { status: 'passed', email_subject: /Successful pipeline/ }, + 1 => { status: 'failed', email_subject: /Failed pipeline/ } + }[exit_code] + end + + def mail_hog_messages(mail_hog_api) + Support::Retrier.retry_until(sleep_interval: 1) do + Runtime::Logger.debug('Fetching email...') + + messages = mail_hog_api.fetch_messages + logs = messages.map { |m| "#{m.to}: #{m.subject}" } + + Runtime::Logger.debug("MailHog Logs: #{logs.join("\n")}") + + # for failing pipelines we have three messages + # one for the owner + # and one for each recipient + messages if mail_hog_pipeline_count(messages) >= 2 + end + end + + def mail_hog_pipeline_count(messages) + messages.count { |message| message.subject.include?('pipeline') } + end + end + end +end diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb index dacfc6c801b..fca14a55468 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_omnibus_spec.rb @@ -172,7 +172,7 @@ module QA Page::Project::Menu.perform(&:go_to_container_registry) Page::Project::Registry::Show.perform do |registry| - expect(registry).to have_registry_repository(project.path_with_namespace) + expect(registry).to have_registry_repository(project.name) registry.click_on_image(project.path_with_namespace) expect(registry).to have_tag('master') @@ -230,7 +230,7 @@ module QA Page::Project::Menu.perform(&:go_to_container_registry) Page::Project::Registry::Show.perform do |registry| - expect(registry).to have_registry_repository(project.path_with_namespace) + expect(registry).to have_registry_repository(project.name) registry.click_on_image(project.path_with_namespace) diff --git a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb index 27b11d697cc..6918c087a4e 100644 --- a/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/container_registry/container_registry_spec.rb @@ -77,9 +77,9 @@ module QA Page::Project::Menu.perform(&:go_to_container_registry) Page::Project::Registry::Show.perform do |registry| - expect(registry).to have_registry_repository(registry_repository.name) + expect(registry).to have_registry_repository(project.name) - registry.click_on_image(registry_repository.name) + registry.click_on_image(project.name) expect(registry).to have_tag('master') registry.click_delete diff --git a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb index 921b36b34af..180910e85a0 100644 --- a/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb +++ b/qa/qa/specs/features/browser_ui/5_package/package_registry/maven/maven_group_level_spec.rb @@ -142,7 +142,7 @@ module QA Page::Group::Menu.perform(&:go_to_package_settings) end - context 'when disabled' do + context 'when enabled' do where do { 'using a personal access token' => { @@ -176,7 +176,7 @@ module QA end before do - Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_disabled) + Page::Group::Settings::PackageRegistries.perform(&:set_reject_duplicates_enabled) end it 'prevents users from publishing group level Maven packages duplicates', testcase: params[:testcase] do @@ -195,7 +195,7 @@ module QA end end - context 'when enabled' do + context 'when disabled' do where do { 'using a personal access token' => { @@ -229,7 +229,7 @@ module QA end before do - Page::Group::Settings::PackageRegistries.perform(&:set_allow_duplicates_enabled) + Page::Group::Settings::PackageRegistries.perform(&:set_reject_duplicates_disabled) end it 'allows users to publish group level Maven packages duplicates', testcase: params[:testcase] do diff --git a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb index 1661fec03be..8b7b827de91 100644 --- a/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb +++ b/qa/qa/specs/features/browser_ui/6_release/deploy_key/add_deploy_key_spec.rb @@ -2,7 +2,7 @@ module QA RSpec.describe 'Release' do - describe 'Deploy key creation', :skip_fips_env do + describe 'Deploy key creation' do it 'user adds a deploy key', testcase: 'https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348023' do Flow::Login.sign_in @@ -15,11 +15,11 @@ module QA resource.key = deploy_key_value end - expect(deploy_key.md5_fingerprint).to eq key.md5_fingerprint + expect(deploy_key.sha256_fingerprint).to eq key.sha256_fingerprint Page::Project::Settings::Repository.perform do |setting| setting.expand_deploy_keys do |keys| - expect(keys).to have_key(deploy_key_title, key.md5_fingerprint) + expect(keys).to have_key(deploy_key_title, key.sha256_fingerprint) 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 ff8dc686991..1f5a431938c 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 @@ -4,7 +4,7 @@ require 'digest/sha1' module QA RSpec.describe 'Release', :runner do - describe 'Git clone using a deploy key', :skip_fips_env do + describe 'Git clone using a deploy key' do let(:runner_name) { "qa-runner-#{SecureRandom.hex(4)}" } let(:repository_location) { project.repository_ssh_location } @@ -33,12 +33,19 @@ module QA end keys = [ - ['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] + ['https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348022', Runtime::Key::RSA, 8192, true], + ['https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348021', Runtime::Key::ECDSA, 521, true], + ['https://gitlab.com/gitlab-org/gitlab/-/quality/test_cases/348020', Runtime::Key::ED25519, false] ] - keys.each do |(testcase, key_class, bits)| + supported_keys = + if QA::Support::FIPS.enabled? + keys.select { |(_, _, _, allowed_in_fips)| allowed_in_fips } + else + keys + end + + supported_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) diff --git a/qa/qa/specs/features/sanity/feature_flags_spec.rb b/qa/qa/specs/features/sanity/feature_flags_spec.rb index 7e68c70ee09..f771978802e 100644 --- a/qa/qa/specs/features/sanity/feature_flags_spec.rb +++ b/qa/qa/specs/features/sanity/feature_flags_spec.rb @@ -20,11 +20,13 @@ module QA describe 'feature flag definition files' do let(:file) do - path = Pathname.new('../config/feature_flags/development').expand_path(Runtime::Path.qa_root) + path = Pathname.new("#{root}/config/feature_flags/development").expand_path(Runtime::Path.qa_root) + path.mkpath Tempfile.new(%w[ff-test .yml], path) end let(:flag) { Pathname.new(file.path).basename('.yml').to_s } + let(:root) { '..'} before do definition = <<~YAML @@ -39,33 +41,47 @@ module QA file.close! end - context 'with a default disabled feature flag' do - let(:flag_enabled) { 'false' } + shared_examples 'gets flag value' do + context 'with a default disabled feature flag' do + let(:flag_enabled) { 'false' } - it 'reads the flag as disabled' do - expect(QA::Runtime::Feature.enabled?(flag)).to be false - end + it 'reads the flag as disabled' do + expect(QA::Runtime::Feature.enabled?(flag)).to be false + end - it 'reads as enabled after the flag is enabled' do - QA::Runtime::Feature.enable(flag) + it 'reads as enabled after the flag is enabled' do + QA::Runtime::Feature.enable(flag) - expect { QA::Runtime::Feature.enabled?(flag) }.to eventually_be_truthy + expect { QA::Runtime::Feature.enabled?(flag) }.to eventually_be_truthy + .within(max_duration: 60, sleep_interval: 5) + end end - end - context 'with a default enabled feature flag' do - let(:flag_enabled) { 'true' } + context 'with a default enabled feature flag' do + let(:flag_enabled) { 'true' } - it 'reads the flag as enabled' do - expect(QA::Runtime::Feature.enabled?(flag)).to be true - end + it 'reads the flag as enabled' do + expect(QA::Runtime::Feature.enabled?(flag)).to be true + end - it 'reads as disabled after the flag is disabled' do - QA::Runtime::Feature.disable(flag) + it 'reads as disabled after the flag is disabled' do + QA::Runtime::Feature.disable(flag) - expect { QA::Runtime::Feature.enabled?(flag) }.to eventually_be_falsey + expect { QA::Runtime::Feature.enabled?(flag) }.to eventually_be_falsey + .within(max_duration: 60, sleep_interval: 5) + end end end + + context 'with a CE feature flag' do + include_examples 'gets flag value' + end + + context 'with an EE feature flag' do + let(:root) { '../ee'} + + include_examples 'gets flag value' + end end end end diff --git a/qa/qa/support/fips.rb b/qa/qa/support/fips.rb new file mode 100644 index 00000000000..f5ebce4fa9c --- /dev/null +++ b/qa/qa/support/fips.rb @@ -0,0 +1,14 @@ +# rubocop: disable Naming/FileName +# frozen_string_literal: true + +module QA + module Support + class FIPS + def self.enabled? + %(1 true yes).include?(ENV['FIPS'].to_s) + end + end + end +end + +# rubocop: enable Naming/FileName diff --git a/qa/qa/support/run.rb b/qa/qa/support/run.rb index a91e7dfd2cb..242293f9eef 100644 --- a/qa/qa/support/run.rb +++ b/qa/qa/support/run.rb @@ -15,6 +15,10 @@ module QA def success? exitstatus == 0 && !response.include?('Error encountered') end + + def to_i + response.to_i + end end def run(command_str, env: [], max_attempts: 1, log_prefix: '') diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb index 6dbbd197b01..4a8fbaf1c4e 100644 --- a/qa/qa/support/waiter.rb +++ b/qa/qa/support/waiter.rb @@ -13,7 +13,8 @@ module QA sleep_interval: 0.1, raise_on_failure: true, retry_on_exception: false, - log: true + log: true, + message: nil ) result = nil repeat_until( @@ -22,7 +23,8 @@ module QA sleep_interval: sleep_interval, raise_on_failure: raise_on_failure, retry_on_exception: retry_on_exception, - log: log + log: log, + message: message ) do result = yield end diff --git a/qa/qa/tools/test_resources_handler.rb b/qa/qa/tools/test_resources_handler.rb index f968fb8b26c..0030a47ed55 100644 --- a/qa/qa/tools/test_resources_handler.rb +++ b/qa/qa/tools/test_resources_handler.rb @@ -31,7 +31,8 @@ module QA 'QA::Resource::CiVariable', 'QA::Resource::Repository::Commit', 'QA::EE::Resource::GroupIteration', - 'QA::EE::Resource::Settings::Elasticsearch' + 'QA::EE::Resource::Settings::Elasticsearch', + 'QA::EE::Resource::VulnerabilityItem' ].freeze PROJECT = 'gitlab-qa-resources' diff --git a/qa/qa/vendor/mail_hog/api.rb b/qa/qa/vendor/mail_hog/api.rb new file mode 100644 index 00000000000..85eb0631624 --- /dev/null +++ b/qa/qa/vendor/mail_hog/api.rb @@ -0,0 +1,75 @@ +# frozen_string_literal: true + +module QA + module Vendor + module MailHog + # Represents a Set of messages from a MailHog response + class Messages + include Enumerable + + attr_reader :data + + def initialize(data) + @data = data + end + + def total + data.dig('total') + end + + def each + data.dig('items')&.each do |item| + yield MessageItem.new(item) + end + end + end + + # Represents an email item from a MailHog response + class MessageItem + attr_reader :data + + def initialize(data) + @data = data + end + + def to + data.dig('Content', 'Headers', 'To', 0) + end + + def subject + data.dig('Content', 'Headers', 'Subject', 0) + end + end + + class API + include Support::API + + attr_reader :hostname + + def initialize(hostname: QA::Runtime::Env.mailhog_hostname || 'localhost') + @hostname = hostname + end + + def base_url + "http://#{hostname}:8025" + end + + def api_messages_url(version: 2) + "#{base_url}/api/v#{version}/messages" + end + + def delete_messages + delete(api_messages_url(version: 1)) + end + + def fetch_messages + Messages.new(JSON.parse(fetch_messages_json)) + end + + def fetch_messages_json + get(api_messages_url).body + end + end + end + end +end diff --git a/qa/spec/runtime/api/request_spec.rb b/qa/spec/runtime/api/request_spec.rb index a1de71d31f0..ea142cfc52e 100644 --- a/qa/spec/runtime/api/request_spec.rb +++ b/qa/spec/runtime/api/request_spec.rb @@ -43,12 +43,13 @@ RSpec.describe QA::Runtime::API::Request do .to eq '/api/v4/users?access_token=otoken' end - it 'respects query parameters' do + it 'respects query parameters', :aggregate_failures do expect(request.request_path('/users?page=1')).to eq '/api/v4/users?page=1' expect(request.request_path('/users', private_token: 'token', foo: 'bar/baz')) .to eq '/api/v4/users?private_token=token&foo=bar%2Fbaz' expect(request.request_path('/users?page=1', private_token: 'token', foo: 'bar/baz')) .to eq '/api/v4/users?page=1&private_token=token&foo=bar%2Fbaz' + expect(request.request_path('/users', per_page: 100)).to eq '/api/v4/users?per_page=100' end it 'uses a different api version' do diff --git a/qa/spec/service/docker_run/base_spec.rb b/qa/spec/service/docker_run/base_spec.rb new file mode 100644 index 00000000000..4c6638cdcf5 --- /dev/null +++ b/qa/spec/service/docker_run/base_spec.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module QA + RSpec.describe Service::DockerRun::Base do + context 'when authenticating' do + let(:instance_one) { Service::DockerRun::Base.new } + let(:instance_two) { Service::DockerRun::Base.new } + + before do + # reset singleton registry state + Service::DockerRun::Base.authenticated_registries.transform_values! { |_v| false } + end + + it 'caches the the registry' do + expect(instance_one).to receive(:shell).once.and_return(nil) + expect(instance_two).not_to receive(:shell) + + instance_one.login('registry.foobar.com', user: 'foobar', password: 'secret') + instance_two.login('registry.foobar.com', user: 'foobar', password: 'secret') + end + + it 'forces authentication if the registry is cached' do + expect(instance_one).to receive(:shell).once.and_return(nil) + expect(instance_two).to receive(:shell).once.and_return(nil) + + instance_one.login('registry.foobar.com', user: 'foobar', password: 'secret') + instance_two.login('registry.foobar.com', user: 'foobar', password: 'secret', force: true) + end + end + end +end diff --git a/qa/spec/service/docker_run/mixins/third_party_docker_spec.rb b/qa/spec/service/docker_run/mixins/third_party_docker_spec.rb new file mode 100644 index 00000000000..5488dca4c40 --- /dev/null +++ b/qa/spec/service/docker_run/mixins/third_party_docker_spec.rb @@ -0,0 +1,68 @@ +# frozen_string_literal: true + +module QA + RSpec.describe QA::Service::DockerRun::Mixins::ThirdPartyDocker do + include QA::Support::Helpers::StubEnv + + let(:klass) do + Class.new(Service::DockerRun::Base) do + include Service::DockerRun::Mixins::ThirdPartyDocker + + def initialize(repo: Runtime::Env.third_party_docker_repository) + @image = "#{repo}/some-image:latest" + end + end + end + + let(:service) { klass.new } + + before do + Service::DockerRun::Base.authenticated_registries.transform_values! { |_v| false } + end + + context 'with environment set' do + before do + stub_env('QA_THIRD_PARTY_DOCKER_REGISTRY', 'registry.foobar.com') + stub_env('QA_THIRD_PARTY_DOCKER_REPOSITORY', 'registry.foobar.com/some/path') + stub_env('QA_THIRD_PARTY_DOCKER_USER', 'username') + stub_env('QA_THIRD_PARTY_DOCKER_PASSWORD', 'secret') + end + + it 'resolves the registry from the environment' do + expect(service.third_party_registry).to eql('registry.foobar.com') + end + + it 'sends a command to authenticate against the registry' do + expect(service).to receive(:shell) + .with( + 'docker login --username "username" --password "secret" registry.foobar.com', + mask_secrets: ['secret'] + ) + .and_return(nil) + + service.authenticate_third_party + end + end + + context 'without environment set' do + before do + stub_env('QA_THIRD_PARTY_DOCKER_REGISTRY', nil) + stub_env('QA_THIRD_PARTY_DOCKER_USER', 'username') + stub_env('QA_THIRD_PARTY_DOCKER_PASSWORD', 'secret') + end + + it 'resolving the registry returns nil' do + expect(service.third_party_registry).to be(nil) + end + + it 'throws if environment is missing' do + expect(service).not_to receive(:shell) + + expect { service.authenticate_third_party }.to raise_error do |err| + expect(err.class).to be(Service::DockerRun::ThirdPartyValidationError) + expect(err.message).to eql('Third party docker environment variable(s) are not set') + end + end + end + end +end diff --git a/qa/spec/service/shellout_spec.rb b/qa/spec/service/shellout_spec.rb index 9d7adeb0e94..52f095f165a 100644 --- a/qa/spec/service/shellout_spec.rb +++ b/qa/spec/service/shellout_spec.rb @@ -13,6 +13,12 @@ module QA allow(Open3).to receive(:popen2e).and_yield(stdin, stdout, wait_thread) end + it 'masks secrets when logging the command itself' do + expect(Runtime::Logger).to receive(:info).with('Executing: `docker login -u **** -p ****`') + expect(wait_thread).to receive(:value).twice.and_return(non_errored_wait) + subject.shell('docker login -u user -p secret', mask_secrets: %w[secret user]) + end + it 'masks command secrets on CommandError' do expect(wait_thread).to receive(:value).twice.and_return(errored_wait) |