From e7550b1deccc8de9d722862ec42c8cda0f18c106 Mon Sep 17 00:00:00 2001
From: Andrejs Cunskis <acunskis@gitlab.com>
Date: Tue, 24 Sep 2024 20:02:46 +0000
Subject: [PATCH] Reset admin password in global hook instead of license
 fabrication class

Use better method name and warning message
---
 qa/qa/ce/strategy.rb                  |  90 +++++++++++++--------
 qa/qa/ee/resource/license.rb          |  15 +---
 qa/qa/ee/strategy.rb                  |  17 ++--
 qa/qa/tools/readiness_check.rb        | 109 ++++++++++++++++++++++++++
 qa/spec/tools/readiness_check_spec.rb |  66 ++++++++++++++++
 5 files changed, 243 insertions(+), 54 deletions(-)
 create mode 100644 qa/qa/tools/readiness_check.rb
 create mode 100644 qa/spec/tools/readiness_check_spec.rb

diff --git a/qa/qa/ce/strategy.rb b/qa/qa/ce/strategy.rb
index 608060ecfeccd..a926c9dac1369 100644
--- a/qa/qa/ce/strategy.rb
+++ b/qa/qa/ce/strategy.rb
@@ -3,49 +3,75 @@
 module QA
   module CE
     module Strategy
-      extend self
-
-      # Perform global setup
-      #
-      # @return [Boolean] returns true if hooks were performed successfully
-      def perform_before_hooks
-        if QA::Runtime::Env.admin_personal_access_token.present?
-          QA::Resource::PersonalAccessTokenCache.set_token_for_username(
-            QA::Runtime::User.admin_username,
-            QA::Runtime::Env.admin_personal_access_token
-          )
+      class << self
+        # Perform global setup
+        #
+        # @return [Boolean] returns true if hooks were performed successfully
+        def perform_before_hooks
+          cache_tokens!
+          log_browser_versions
+
+          if Runtime::Env.rspec_retried?
+            Runtime::Logger.info('Skipping global hooks due to retry process')
+            return false
+          end
+
+          # Perform app readiness check before continuing with the whole test suite
+          Tools::ReadinessCheck.perform(wait: 60)
+
+          # Reset admin password if admin token is present but can't be used due to expired password
+          reset_admin_password!
+
+          if Runtime::Env.allow_local_requests?
+            Runtime::ApplicationSettings.set_application_settings(
+              allow_local_requests_from_web_hooks_and_services: true
+            )
+          end
+
+          true
         end
 
-        if QA::Runtime::Env.personal_access_token.present? && QA::Runtime::Env.user_username.present?
-          QA::Resource::PersonalAccessTokenCache.set_token_for_username(
-            QA::Runtime::Env.user_username,
-            QA::Runtime::Env.personal_access_token
+        private
+
+        def cache_tokens!
+          if Runtime::Env.admin_personal_access_token.present?
+            Resource::PersonalAccessTokenCache.set_token_for_username(
+              Runtime::User.admin_username,
+              Runtime::Env.admin_personal_access_token
+            )
+          end
+
+          return unless Runtime::Env.personal_access_token.present? && Runtime::Env.user_username.present?
+
+          Resource::PersonalAccessTokenCache.set_token_for_username(
+            Runtime::Env.user_username,
+            Runtime::Env.personal_access_token
           )
         end
 
-        QA::Runtime::Logger.info("Using Browser: #{QA::Runtime::Env.browser}")
+        def log_browser_versions
+          Runtime::Logger.info("Using Browser: #{Runtime::Env.browser}")
+          return unless Runtime::Env.use_selenoid?
 
-        if QA::Runtime::Env.use_selenoid?
-          QA::Runtime::Logger.info("Using Selenoid Browser version: #{QA::Runtime::Env.selenoid_browser_version}")
+          Runtime::Logger.info("Using Selenoid Browser version: #{Runtime::Env.selenoid_browser_version}")
         end
 
-        if Runtime::Env.rspec_retried?
-          Runtime::Logger.info('Skipping global hooks due to retry process')
-          return false
-        end
+        def reset_admin_password!
+          return unless Runtime::Env.admin_personal_access_token.present?
 
-        # The login page could take some time to load the first time it is visited.
-        # We visit the login page and wait for it to properly load only once before the tests.
-        QA::Runtime::Logger.info("Performing sanity check for environment!")
-        QA::Support::Retrier.retry_on_exception do
-          QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login)
-        end
+          response = Support::API.get(Runtime::API::Request.new(Runtime::API::Client.as_admin, "/user").url)
+          return unless response.code == 403 && response.body.include?("Your password expired")
 
-        if QA::Runtime::Env.allow_local_requests?
-          Runtime::ApplicationSettings.set_application_settings(allow_local_requests_from_web_hooks_and_services: true)
-        end
+          # Mostly issue with gdk where default seeded password for admin user will be expired
+          Runtime::Logger.warn(
+            "Admin password must be reset before the configured access token can be used. Setting password now..."
+          )
 
-        true
+          Runtime::Browser.visit(:gitlab, Page::Main::Login)
+          Page::Main::Login.perform(&:sign_in_using_admin_credentials)
+          Page::Main::Login.perform(&:set_up_new_admin_password_if_required)
+          Page::Main::Menu.perform(&:sign_out_if_signed_in)
+        end
       end
     end
   end
diff --git a/qa/qa/ee/resource/license.rb b/qa/qa/ee/resource/license.rb
index 7b1904399a417..c5e2242bce863 100644
--- a/qa/qa/ee/resource/license.rb
+++ b/qa/qa/ee/resource/license.rb
@@ -40,6 +40,7 @@ def initialize
         end
 
         def fabricate!
+          QA::Page::Main::Menu.perform(&:sign_out_if_signed_in)
           QA::Page::Main::Login.perform(&:sign_in_using_admin_credentials)
           QA::Page::Main::Menu.perform(&:go_to_admin_area)
           QA::Page::Main::Login.perform(&:set_up_new_admin_password_if_required)
@@ -83,18 +84,8 @@ def fabricate_via_api!
 
             api_post.tap { QA::Runtime::Logger.info("Successfully added license key. Details:\n#{license_info}") }
           rescue RuntimeError => e
-            unless e.message.include?('Your password expired')
-              QA::Runtime::Logger.error("Following license fabrication failed: #{base_license_info}")
-              raise(e)
-            end
-
-            QA::Runtime::Logger.warn('Admin password must be reset before the default access token can be used. ' \
-                                     'Setting password now...')
-
-            QA::Page::Main::Login.perform(&:sign_in_using_admin_credentials)
-            QA::Page::Main::Login.perform(&:set_up_new_admin_password_if_required)
-
-            retry
+            QA::Runtime::Logger.error("Following license fabrication failed: #{base_license_info}")
+            raise(e)
           end
         end
 
diff --git a/qa/qa/ee/strategy.rb b/qa/qa/ee/strategy.rb
index 61ff4bd51956e..a4efc2e071ee8 100644
--- a/qa/qa/ee/strategy.rb
+++ b/qa/qa/ee/strategy.rb
@@ -12,23 +12,20 @@ def perform_before_hooks
 
         if QA::Runtime::Env.ee_license.present?
           QA::Runtime::Logger.info("Performing initial license fabrication!")
-          QA::Page::Main::Menu.perform(&:sign_out_if_signed_in)
 
           EE::Resource::License.fabricate! do |resource|
             resource.license = QA::Runtime::Env.ee_license
           end
         end
 
-        unless QA::Runtime::Env.running_on_dot_com?
-          QA::Runtime::Logger.info("Disabling sync with External package metadata database")
-          # we can't pass [] here, otherwise it causes a validation error, because the value we pass
-          # must be a valid purl_type. Instead, we pass the `deb` purl_type which is only used for
-          # container scanning advisories, which are not yet supported/ingested, so this is effectively
-          # the same thing as disabling the sync.
-          QA::Runtime::ApplicationSettings.set_application_settings(package_metadata_purl_types: [DEB_PURL_TYPE])
-        end
+        return if QA::Runtime::Env.running_on_dot_com?
 
-        QA::Page::Main::Menu.perform(&:sign_out_if_signed_in)
+        QA::Runtime::Logger.info("Disabling sync with External package metadata database")
+        # we can't pass [] here, otherwise it causes a validation error, because the value we pass
+        # must be a valid purl_type. Instead, we pass the `deb` purl_type which is only used for
+        # container scanning advisories, which are not yet supported/ingested, so this is effectively
+        # the same thing as disabling the sync.
+        QA::Runtime::ApplicationSettings.set_application_settings(package_metadata_purl_types: [DEB_PURL_TYPE])
       end
     end
   end
diff --git a/qa/qa/tools/readiness_check.rb b/qa/qa/tools/readiness_check.rb
new file mode 100644
index 0000000000000..d15f11dc79d5b
--- /dev/null
+++ b/qa/qa/tools/readiness_check.rb
@@ -0,0 +1,109 @@
+# frozen_string_literal: true
+
+require "nokogiri"
+
+module QA
+  module Tools
+    # Helper to assert GitLab instance readiness without starting a web server
+    #
+    class ReadinessCheck
+      include Support::API
+
+      def self.perform(wait: 60)
+        new(wait: wait).perform
+      end
+
+      def initialize(wait:)
+        @wait = wait
+      end
+
+      # Validate gitlab readiness via check for presence of sign-in-form element
+      #
+      # @return [void]
+      def perform
+        error = nil
+
+        info("Waiting for Gitlab to become ready!")
+        Support::Retrier.retry_until(max_duration: wait, sleep_interval: 1, raise_on_failure: false, log: false) do
+          result = !required_elements_missing?
+          error = nil
+
+          result
+        rescue StandardError => e
+          error = "#{error_base} #{e.message}"
+
+          false
+        end
+        raise error if error
+
+        info("Gitlab is ready!")
+      end
+
+      private
+
+      delegate :debug, :info, to: QA::Runtime::Logger
+
+      attr_reader :wait
+
+      # Sign in page url
+      #
+      # @return [String]
+      def url
+        @url ||= "#{Support::GitlabAddress.address_with_port(with_default_port: false)}/users/sign_in"
+      end
+
+      # Error message base
+      #
+      # @return [String]
+      def error_base
+        @error_base ||= "Gitlab readiness check failed, valid sign_in page did not appear within #{wait} seconds!"
+      end
+
+      # Required elements css selectors
+      #
+      # @return [Array<String>]
+      def elements_css
+        @element_css ||= QA::Page::Main::Login.elements.select(&:required?).map(&:selector_css)
+      end
+
+      # Check for missing required elements on sign-in page
+      #
+      # @return [Boolean]
+      def required_elements_missing?
+        debug("Checking for required element presence on '#{url}'")
+        response = get(url)
+
+        unless ok_response?(response)
+          msg = "Got unsucessfull response code: #{response.code}"
+          debug(msg) && raise(msg)
+        end
+
+        unless required_elements_present?(response)
+          msg = "Sign in page missing required elements: '#{elements_css}'"
+          debug(msg) && raise(msg)
+        end
+
+        debug("Required elements are present!")
+        false
+      end
+
+      # Validate response code is 200
+      #
+      # @param [RestClient::Response] response
+      # @return [Boolean]
+      def ok_response?(response)
+        response.code == Support::API::HTTP_STATUS_OK
+      end
+
+      # Check required elements are present on sign-in page
+      #
+      # @param [RestClient::Response] response
+      # @return [Boolean]
+      def required_elements_present?(response)
+        doc = Nokogiri::HTML.parse(response.body)
+
+        elements_css.all? { |sel| doc.css(sel).any? }
+      end
+    end
+  end
+end
diff --git a/qa/spec/tools/readiness_check_spec.rb b/qa/spec/tools/readiness_check_spec.rb
new file mode 100644
index 0000000000000..32abb6aea6db0
--- /dev/null
+++ b/qa/spec/tools/readiness_check_spec.rb
@@ -0,0 +1,66 @@
+# frozen_string_literal: true
+
+RSpec.describe QA::Tools::ReadinessCheck do
+  subject(:readiness_check) { described_class.new(wait: wait) }
+
+  let(:url) { "example.com" }
+  let(:wait) { 1 }
+  let(:msg_base) { "Gitlab readiness check failed, valid sign_in page did not appear within #{wait} seconds!" }
+
+  let(:response) { instance_double(RestClient::Response, code: code, body: body) }
+  let(:code) { 200 }
+  let(:body) { "" }
+
+  before do
+    allow(QA::Support::GitlabAddress).to receive(:address_with_port).with(with_default_port: false).and_return(url)
+    allow(readiness_check).to receive(:get).with("#{url}/users/sign_in").and_return(response)
+  end
+
+  context "with successfull response" do
+    let(:body) do
+      <<~HTML
+        <!DOCTYPE html>
+          <body data-testid="login-page">
+          </body>
+        </html>
+      HTML
+    end
+
+    it "validates readiness" do
+      expect { readiness_check.perform }.not_to raise_error
+    end
+  end
+
+  context "with missing sign in form" do
+    let(:body) do
+      <<~HTML
+        <!DOCTYPE html>
+        </html>
+      HTML
+    end
+
+    it "raises an error on validation" do
+      expect { readiness_check.perform }.to raise_error(/#{msg_base} Sign in page missing required elements/)
+    end
+  end
+
+  context "with unsuccessfull response code" do
+    let(:code) { 500 }
+
+    it "raises an error on validation" do
+      expect { readiness_check.perform }.to raise_error(
+        "#{msg_base} Got unsucessfull response code: #{code}"
+      )
+    end
+  end
+
+  context "with request timeout" do
+    before do
+      allow(readiness_check).to receive(:get).and_raise(RestClient::Exceptions::OpenTimeout)
+    end
+
+    it "raises an error on validation" do
+      expect { readiness_check.perform }.to raise_error("#{msg_base} Timed out connecting to server")
+    end
+  end
+end
-- 
GitLab