From 190a0fba5fb4ac3fd1416c2b97f24d64fe89c4fc Mon Sep 17 00:00:00 2001
From: Andrejs Cunskis <acunskis@gitlab.com>
Date: Wed, 25 Sep 2024 06:24:56 +0000
Subject: [PATCH] Implement headless readiness check for gitlab

Use web browser for sign in page check on dot_com environments

Add specs for dot_com scenario
---
 qa/qa/ce/strategy.rb                  |   5 +-
 qa/qa/tools/readiness_check.rb        | 124 ++++++++++++++++++++++++++
 qa/spec/tools/readiness_check_spec.rb |  92 +++++++++++++++++++
 3 files changed, 218 insertions(+), 3 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 93b94db840fea..a926c9dac1369 100644
--- a/qa/qa/ce/strategy.rb
+++ b/qa/qa/ce/strategy.rb
@@ -16,9 +16,8 @@ def perform_before_hooks
             return false
           end
 
-          QA::Support::Retrier.retry_on_exception do
-            QA::Runtime::Browser.visit(:gitlab, QA::Page::Main::Login)
-          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!
diff --git a/qa/qa/tools/readiness_check.rb b/qa/qa/tools/readiness_check.rb
new file mode 100644
index 0000000000000..ed6962d0f1c45
--- /dev/null
+++ b/qa/qa/tools/readiness_check.rb
@@ -0,0 +1,124 @@
+# frozen_string_literal: true
+
+require "nokogiri"
+
+module QA
+  module Tools
+    # Helper to assert GitLab instance readiness without starting a web browser
+    #
+    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}'")
+        # Do not perform headless request on .com due to cloudfare
+        return rendered_elements_missing? if Runtime::Env.running_on_dot_com?
+
+        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
+
+      # Perform check for present elements via web browser
+      #
+      # @return [Boolean]
+      def rendered_elements_missing?
+        debug("Checking for required elements via web browser")
+        Runtime::Browser.visit(:gitlab, Page::Main::Login)
+        false
+      rescue StandardError => e
+        debug("Sign in page did not render fully")
+        raise(e)
+      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..75c7e1290f9d4
--- /dev/null
+++ b/qa/spec/tools/readiness_check_spec.rb
@@ -0,0 +1,92 @@
+# 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(:dot_com) { false }
+
+  let(:response) { instance_double(RestClient::Response, code: code, body: body) }
+  let(:code) { 200 }
+  let(:body) { "" }
+
+  before do
+    allow(QA::Runtime::Env).to receive(:running_on_dot_com?).and_return(dot_com)
+    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
+
+  context "when running on dot com" do
+    let(:dot_com) { true }
+
+    context "with successfull check" do
+      before do
+        allow(QA::Runtime::Browser).to receive(:visit).with(:gitlab, QA::Page::Main::Login)
+      end
+
+      it "validates readiness" do
+        expect { readiness_check.perform }.not_to raise_error
+      end
+    end
+
+    context "with unsuccessfull check" do
+      before do
+        allow(QA::Runtime::Browser).to receive(:visit).with(:gitlab, QA::Page::Main::Login).and_raise("not loaded")
+      end
+
+      it "raises an error on validation" do
+        expect { readiness_check.perform }.to raise_error("#{msg_base} not loaded")
+      end
+    end
+  end
+end
-- 
GitLab