KNOX-3293: Add integration tests: health metrics JSON, KnoxLDAP preauth/extauthz paths, RemoteAuth extauthz (#1194)

* KNOX-3293: Add integration tests: health metrics JSON, KnoxLDAP preauth/extauthz paths, RemoteAuth extauthz

* Refactoered tests and made a common util file
diff --git a/.github/workflows/tests/common_utils.py b/.github/workflows/tests/common_utils.py
new file mode 100644
index 0000000..41198b0
--- /dev/null
+++ b/.github/workflows/tests/common_utils.py
@@ -0,0 +1,68 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import annotations
+
+import os
+import unittest
+from typing import Any
+
+import requests
+import urllib3
+
+# Default timeout for HTTP calls to the gateway (self-signed TLS, CI).
+KNOX_REQUEST_TIMEOUT = 30
+
+HSTS_HEADER_NAME = "Strict-Transport-Security"
+HSTS_EXPECTED_VALUE = "max-age=300; includeSubDomains"
+
+urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+
+
+def gateway_base_url() -> str:
+    """Return KNOX_GATEWAY_URL with a trailing slash."""
+    url = os.environ.get("KNOX_GATEWAY_URL", "https://localhost:8443/")
+    return url if url.endswith("/") else (url + "/")
+
+
+def knox_get(url: str, **kwargs: Any) -> requests.Response:
+    """GET against Knox with verify=False and default timeout unless overridden."""
+    opts: dict[str, Any] = {"verify": False, "timeout": KNOX_REQUEST_TIMEOUT}
+    opts.update(kwargs)
+    return requests.get(url, **opts)
+
+
+def knox_post(url: str, **kwargs: Any) -> requests.Response:
+    """POST against Knox with verify=False and default timeout unless overridden."""
+    opts: dict[str, Any] = {"verify": False, "timeout": KNOX_REQUEST_TIMEOUT}
+    opts.update(kwargs)
+    return requests.post(url, **opts)
+
+
+def collect_actor_group_values(
+    response: requests.Response, prefix: str = "x-knox-actor-groups"
+) -> list[str]:
+    """Comma-split values from all response headers whose names start with prefix (case-insensitive)."""
+    prefix_lower = prefix.lower()
+    all_groups: list[str] = []
+    for name in response.headers:
+        if name.lower().startswith(prefix_lower):
+            all_groups.extend(response.headers[name].split(","))
+    return all_groups
+
+
+def assert_hsts_header(testcase: unittest.TestCase, response: requests.Response) -> None:
+    testcase.assertIn(HSTS_HEADER_NAME, response.headers)
+    testcase.assertEqual(response.headers[HSTS_HEADER_NAME], HSTS_EXPECTED_VALUE)
diff --git a/.github/workflows/tests/test_health.py b/.github/workflows/tests/test_health.py
index 421dc52..18acc30 100644
--- a/.github/workflows/tests/test_health.py
+++ b/.github/workflows/tests/test_health.py
@@ -12,31 +12,47 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import os
+import json
 import unittest
-import requests
-import urllib3
 
-# Suppress InsecureRequestWarning
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
+import requests
+
+from common_utils import assert_hsts_header, gateway_base_url, knox_get
+
 
 class TestKnoxHealth(unittest.TestCase):
-    def test_admin_api_health(self):
+    def setUp(self):
+        self.base_url = gateway_base_url()
+
+    def test_health_ping_ok_and_hsts(self):
         """
         Basic health check to ensure Knox is up and running.
-        We expect a response 200 to indicate the server is up.
         """
-        url = os.environ.get("KNOX_GATEWAY_URL", "https://localhost:8443/")
+        url = self.base_url + "gateway/health/v1/ping"
         print(f"Checking connectivity to {url}...")
         try:
-            response = requests.get(url + "health/v1/ping", verify=False, timeout=30)
+            response = knox_get(url)
             print(f"Received status code: {response.status_code}")
             self.assertEqual(response.status_code, 200)
+            self.assertEqual(response.text.strip(), "OK")
+
+            assert_hsts_header(self, response)
         except requests.exceptions.ConnectionError:
             self.fail("Failed to connect to Knox on port 8443 - Connection refused")
         except Exception as e:
             self.fail(f"Health check failed with unexpected error: {e}")
 
+    def test_health_metrics_returns_json(self):
+        url = self.base_url + "gateway/health/v1/metrics?pretty=true"
+        response = knox_get(url)
+        self.assertEqual(response.status_code, 200)
+
+        content_type = response.headers.get("Content-Type", "")
+        self.assertIn("application/json", content_type)
+
+        payload = json.loads(response.text)
+        self.assertIsInstance(payload, dict)
+
 if __name__ == '__main__':
     unittest.main()
 
diff --git a/.github/workflows/tests/test_knox_auth_service_and_LDAP.py b/.github/workflows/tests/test_knox_auth_service_and_LDAP.py
index dd1b344..51a42d8 100644
--- a/.github/workflows/tests/test_knox_auth_service_and_LDAP.py
+++ b/.github/workflows/tests/test_knox_auth_service_and_LDAP.py
@@ -12,12 +12,11 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import os
 import unittest
-import requests
-import urllib3
 from requests.auth import HTTPBasicAuth
 
+from common_utils import collect_actor_group_values, gateway_base_url, knox_get
+
 ########################################################
 # This test is verifying the behavior of the Knox Auth Service + LDAP authentication.
 # It is using the 'auth/api/v1/pre' endpoint to get the actor ID and group headers.
@@ -28,14 +27,9 @@
 # It is verifying that the actor ID and group headers are not None.
 ########################################################
 
-# Suppress InsecureRequestWarning
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
 class TestKnoxAuthService(unittest.TestCase):
     def setUp(self):
-        self.base_url = os.environ.get("KNOX_GATEWAY_URL", "https://localhost:8443/")
-        if not self.base_url.endswith("/"):
-            self.base_url += "/"
+        self.base_url = gateway_base_url()
         # The topology name is based on the filename knoxldap.xml
         self.topology_url = self.base_url + "gateway/knoxldap/auth/api/v1/extauthz"
 
@@ -44,11 +38,9 @@
         Verify that guest user gets the correct actor ID header.
         """
         print(f"\nTesting guest authentication against {self.topology_url}")
-        response = requests.get(
-            self.topology_url, 
+        response = knox_get(
+            self.topology_url,
             auth=HTTPBasicAuth('guest', 'guest-password'),
-            verify=False, 
-            timeout=30
         )
         
         print(f"Status Code: {response.status_code}")
@@ -66,11 +58,9 @@
         Verify that admin user gets actor ID and group headers.
         """
         print(f"\nTesting admin authentication against {self.topology_url}")
-        response = requests.get(
-            self.topology_url, 
+        response = knox_get(
+            self.topology_url,
             auth=HTTPBasicAuth('admin', 'admin-password'),
-            verify=False, 
-            timeout=30
         )
         
         print(f"Status Code: {response.status_code}")
@@ -82,22 +72,15 @@
         self.assertEqual(response.headers[actor_id_header], 'admin')
         print(f"Verified {actor_id_header}: {response.headers[actor_id_header]}")
         
-        # Check for Group headers
         # Config: 'preauth.auth.header.actor.groups.prefix' = 'x-knox-actor-groups'
         # We mapped admin to 'longGroupName1,longGroupName2,longGroupName3,longGroupName4'
-        
-        # We just verify that at least one header starting with the prefix exists
         prefix = 'x-knox-actor-groups'
-        group_headers = [h for h in response.headers.keys() if h.lower().startswith(prefix.lower())]
-        
-        self.assertTrue(len(group_headers) > 0, f"No headers found starting with {prefix}")
-        
-        # Verify content of groups
-        all_groups = []
-        for h in group_headers:
-            all_groups.extend(response.headers[h].split(','))
-            print(f"Found group header {h}: {response.headers[h]}")
-            
+        all_groups = collect_actor_group_values(response, prefix=prefix)
+        self.assertTrue(len(all_groups) > 0, f"No headers found starting with {prefix}")
+        for h in response.headers:
+            if h.lower().startswith(prefix.lower()):
+                print(f"Found group header {h}: {response.headers[h]}")
+
         expected_groups = ['longGroupName1', 'longGroupName2', 'longGroupName3', 'longGroupName4']
         for group in expected_groups:
             self.assertIn(group, all_groups)
diff --git a/.github/workflows/tests/test_knox_configs.py b/.github/workflows/tests/test_knox_configs.py
index 45947f0..0a9b5e7 100644
--- a/.github/workflows/tests/test_knox_configs.py
+++ b/.github/workflows/tests/test_knox_configs.py
@@ -12,12 +12,11 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import os
 import unittest
-import requests
-import urllib3
 from requests.auth import HTTPBasicAuth
 
+from common_utils import assert_hsts_header, gateway_base_url, knox_get
+
 
 ########################################################
 # This test is verifying the global HSTS headers for 404 response.
@@ -25,14 +24,9 @@
 # It verifies header is present with the correct value.
 ########################################################
 
-# Suppress InsecureRequestWarning
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
 class TestKnoxConfigs(unittest.TestCase):
     def setUp(self):
-        self.base_url = os.environ.get("KNOX_GATEWAY_URL", "https://localhost:8443/")
-        if not self.base_url.endswith("/"):
-            self.base_url += "/"
+        self.base_url = gateway_base_url()
         self.non_existent_path = self.base_url + "gateway/not-exists"
 
     def test_auth_service_guest(self):
@@ -40,18 +34,14 @@
         Verifies header is present with the correct value
         """
         print(f"\nTesting global HSTS config for 404 response")
-        response = requests.get(
+        response = knox_get(
             self.non_existent_path,
             auth=HTTPBasicAuth('admin', 'admin-password'),
-            verify=False,
-            timeout=30
         )
 
         print(f"Status Code: {response.status_code}")
         self.assertEqual(response.status_code, 404)
 
-        hsts_header = 'Strict-Transport-Security'
-        self.assertIn(hsts_header, response.headers)
-        self.assertEqual(response.headers[hsts_header], 'max-age=300; includeSubDomains')
-        print(f"Verified {hsts_header}: {response.headers[hsts_header]}")
+        assert_hsts_header(self, response)
+        print(f"Verified Strict-Transport-Security: {response.headers['Strict-Transport-Security']}")
 
diff --git a/.github/workflows/tests/test_knoxauth_preauth_and_paths.py b/.github/workflows/tests/test_knoxauth_preauth_and_paths.py
new file mode 100644
index 0000000..ce9b2f9
--- /dev/null
+++ b/.github/workflows/tests/test_knoxauth_preauth_and_paths.py
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from requests.auth import HTTPBasicAuth
+
+from common_utils import gateway_base_url, knox_get, knox_post
+
+
+class TestKnoxAuthServicePreAuthAndPaths(unittest.TestCase):
+    def setUp(self):
+        self.base_url = gateway_base_url()
+        self.preauth_url = self.base_url + "gateway/knoxldap/auth/api/v1/pre"
+        self.extauthz_url = self.base_url + "gateway/knoxldap/auth/api/v1/extauthz"
+
+    def test_preauth_requires_auth(self):
+        response = knox_get(self.preauth_url)
+        self.assertEqual(response.status_code, 401)
+
+    def test_preauth_bad_credentials_unauthorized(self):
+        response = knox_get(
+            self.preauth_url,
+            auth=HTTPBasicAuth("baduser", "badpass"),
+        )
+        self.assertEqual(response.status_code, 401)
+
+    def test_preauth_post_supported(self):
+        response = knox_post(
+            self.preauth_url,
+            auth=HTTPBasicAuth("guest", "guest-password"),
+        )
+        self.assertEqual(response.status_code, 200)
+
+        actor_id_header = "x-knox-actor-username"
+        self.assertIn(actor_id_header, response.headers)
+        self.assertEqual(response.headers[actor_id_header], "guest")
+
+    def test_extauthz_additional_path_not_ignored_in_knoxldap(self):
+        response = knox_get(
+            self.extauthz_url + "/does-not-exist",
+            auth=HTTPBasicAuth("guest", "guest-password"),
+        )
+        self.assertEqual(response.status_code, 404)
+
+
+if __name__ == "__main__":
+    unittest.main()
+
diff --git a/.github/workflows/tests/test_remote_auth.py b/.github/workflows/tests/test_remote_auth.py
index 38cf180..ae87c53 100644
--- a/.github/workflows/tests/test_remote_auth.py
+++ b/.github/workflows/tests/test_remote_auth.py
@@ -12,12 +12,11 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
-import os
 import unittest
-import requests
-import urllib3
 from requests.auth import HTTPBasicAuth
 
+from common_utils import collect_actor_group_values, gateway_base_url, knox_get
+
 ########################################################
 # This test is verifying the behavior of the RemoteAuthProvider.
 # It is using the 'auth/api/v1/pre' endpoint to get the actor ID and group headers.
@@ -27,14 +26,9 @@
 # It is verifying that the actor ID and group headers are not empty.
 # It is verifying that the actor ID and group headers are not None.
 ########################################################
-# Suppress InsecureRequestWarning
-urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
-
 class TestRemoteAuth(unittest.TestCase):
     def setUp(self):
-        self.base_url = os.environ.get("KNOX_GATEWAY_URL", "https://localhost:8443/")
-        if not self.base_url.endswith("/"):
-            self.base_url += "/"
+        self.base_url = gateway_base_url()
         self.topology_url = self.base_url + "gateway/remoteauth/auth/api/v1/pre"
 
     def test_remote_auth_success(self):
@@ -45,11 +39,9 @@
         print(f"\nTesting remote auth success against {self.topology_url}")
         # The RemoteAuthFilter forwards Authorization header to knoxldap.
         # knoxldap accepts guest:guest-password
-        response = requests.get(
+        response = knox_get(
             self.topology_url,
             auth=HTTPBasicAuth('guest', 'guest-password'),
-            verify=False,
-            timeout=30
         )
         print(f"Status Code: {response.status_code}")
         print(f"Headers: {response.headers}")
@@ -65,11 +57,9 @@
         Verify admin user gets multiple groups from knoxldap mapping
         """
         print(f"\nTesting remote auth admin against {self.topology_url}")
-        response = requests.get(
+        response = knox_get(
             self.topology_url,
             auth=HTTPBasicAuth('admin', 'admin-password'),
-            verify=False,
-            timeout=30
         )
         self.assertEqual(response.status_code, 200)
         self.assertEqual(response.headers['X-Knox-Actor-ID'], 'admin')
@@ -78,11 +68,8 @@
         # RemoteAuthFilter picks these up from x-knox-actor-groups-*
         # And KNOX-AUTH-SERVICE echoes them back in X-Knox-Actor-Groups-*
         
-        group_headers = [h for h in response.headers if h.lower().startswith('x-knox-actor-groups')]
-        all_groups = []
-        for h in group_headers:
-            all_groups.extend(response.headers[h].split(','))
-        
+        all_groups = collect_actor_group_values(response)
+
         print(f"Found groups: {all_groups}")
         self.assertIn('longGroupName1', all_groups)
         self.assertIn('longGroupName2', all_groups)
@@ -92,11 +79,9 @@
         Verify invalid credentials result in 401
         """
         print(f"\nTesting remote auth failure against {self.topology_url}")
-        response = requests.get(
+        response = knox_get(
             self.topology_url,
             auth=HTTPBasicAuth('baduser', 'badpass'),
-            verify=False,
-            timeout=30
         )
         print(f"Status Code: {response.status_code}")
         # When remote auth fails (knoxldap returns 401), RemoteAuthFilter should return 401
diff --git a/.github/workflows/tests/test_remoteauth_extauthz_additional_path.py b/.github/workflows/tests/test_remoteauth_extauthz_additional_path.py
new file mode 100644
index 0000000..73a6f57
--- /dev/null
+++ b/.github/workflows/tests/test_remoteauth_extauthz_additional_path.py
@@ -0,0 +1,60 @@
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements.  See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to you under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License.  You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import unittest
+
+from requests.auth import HTTPBasicAuth
+
+from common_utils import gateway_base_url, knox_get
+
+
+class TestRemoteAuthExtAuthzAdditionalPath(unittest.TestCase):
+    def setUp(self):
+        self.base_url = gateway_base_url()
+        self.extauthz_url = self.base_url + "gateway/remoteauth/auth/api/v1/extauthz"
+
+    def test_extauthz_success(self):
+        response = knox_get(
+            self.extauthz_url,
+            auth=HTTPBasicAuth("guest", "guest-password"),
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("X-Knox-Actor-ID", response.headers)
+        self.assertEqual(response.headers["X-Knox-Actor-ID"], "guest")
+
+    def test_extauthz_additional_path_is_ignored(self):
+        response = knox_get(
+            self.extauthz_url + "/some/extra/path",
+            auth=HTTPBasicAuth("guest", "guest-password"),
+        )
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("X-Knox-Actor-ID", response.headers)
+        self.assertEqual(response.headers["X-Knox-Actor-ID"], "guest")
+
+    def test_extauthz_bad_credentials_unauthorized(self):
+        response = knox_get(
+            self.extauthz_url,
+            auth=HTTPBasicAuth("baduser", "badpass"),
+        )
+        self.assertEqual(response.status_code, 401)
+
+    def test_extauthz_missing_credentials_server_error(self):
+        # No Authorization header: RemoteAuth hits an error path (500), not 401.
+        response = knox_get(self.extauthz_url)
+        self.assertEqual(response.status_code, 500)
+
+
+if __name__ == "__main__":
+    unittest.main()
+