Repository: SAP/data-attribute-recommendation-python-sdk

Issue:
Allow http://localhost for local testing; skip HTTPSRequired exception

Discussion:

Patch:
diff --git a/.flake8 b/.flake8
index 53496e9..307dd28 100644
--- a/.flake8
+++ b/.flake8
@@ -3,4 +3,5 @@
 max-line-length = 88
 # E203 clashes with black, see https://github.com/PyCQA/pycodestyle/pull/914
 # W503 is already deprecated: https://www.flake8rules.com/rules/W503.html
-ignore = E203, W503
+# B011: pytest uses assert, so we should allow that
+ignore = E203, W503, B011
diff --git a/sap/aibus/dar/client/dar_session.py b/sap/aibus/dar/client/dar_session.py
index 01566d3..10f3456 100644
--- a/sap/aibus/dar/client/dar_session.py
+++ b/sap/aibus/dar/client/dar_session.py
@@ -12,7 +12,7 @@ from sap.aibus.dar.client.util.credentials import CredentialsSource
 from sap.aibus.dar.client.util.http_transport import (
     TimeoutRetrySession,
     TimeoutPostRetrySession,
-    enforce_https,
+    enforce_https_except_localhost,
 )
 
 
@@ -56,7 +56,7 @@ class DARSession:
         if base_url[-1] == "/":
             # Normalize base url.
             base_url = base_url[:-1]
-        enforce_https(base_url)
+        enforce_https_except_localhost(base_url)
         self.base_url = base_url
         self.credentials_source = credentials_source
         self.http = TimeoutRetrySession()
diff --git a/sap/aibus/dar/client/util/credentials.py b/sap/aibus/dar/client/util/credentials.py
index 25709de..a1caf62 100644
--- a/sap/aibus/dar/client/util/credentials.py
+++ b/sap/aibus/dar/client/util/credentials.py
@@ -11,7 +11,7 @@ from typing import Callable
 from sap.aibus.dar.client.util.http_transport import (
     HttpMethodsProtocol,
     TimeoutRetrySession,
-    enforce_https,
+    enforce_https_except_localhost,
 )
 from sap.aibus.dar.client.util.logging import LoggerMixin
 
@@ -96,7 +96,7 @@ class OnlineCredentialsSource(CredentialsSource, LoggerMixin):
         """
         # pylint: disable=too-many-arguments
 
-        enforce_https(url)
+        enforce_https_except_localhost(url)
 
         self._token = None
         self._token_expires_at = 0
diff --git a/sap/aibus/dar/client/util/http_transport.py b/sap/aibus/dar/client/util/http_transport.py
index 6087391..16fd520 100644
--- a/sap/aibus/dar/client/util/http_transport.py
+++ b/sap/aibus/dar/client/util/http_transport.py
@@ -69,7 +69,7 @@ class HttpMethodsMixin(HttpMethodsProtocol):
         return {}
 
     def __handle(self, verb, url, *args, **kwargs):
-        enforce_https(url)
+        enforce_https_except_localhost(url)
         kwargs.update(self.default_kwargs())
         return getattr(self.session, verb)(url, *args, **kwargs)
 
@@ -331,7 +331,7 @@ class TimeoutPostRetrySession(TimeoutRetrySession):
         return PostRetrySession(num_retries)
 
 
-def enforce_https(url: str):
+def enforce_https_except_localhost(url: str):
     """
     Raises HTTPSRequired exception if required.
 
@@ -339,5 +339,5 @@ def enforce_https(url: str):
     :return: None
     :raises HTTPSRequired: if given url does not start with https
     """
-    if not url.startswith("https"):
+    if not url.startswith("https") and not url.startswith("http://localhost"):
         raise HTTPSRequired


Test patch:
diff --git a/tests/sap/aibus/dar/client/test_dar_session.py b/tests/sap/aibus/dar/client/test_dar_session.py
index 36cc7e5..264055e 100644
--- a/tests/sap/aibus/dar/client/test_dar_session.py
+++ b/tests/sap/aibus/dar/client/test_dar_session.py
@@ -251,3 +251,12 @@ class TestHTTPSEnforced:
             "URL must use https scheme. Unencrypted connections" " are not supported."
         )
         assert str(context.value) == expected_message
+
+    def test_localhost_is_allowed(self):
+        dar_url = "http://localhost/"
+        credentials_source = StaticCredentialsSource("12345")
+
+        try:
+            DARSession(dar_url, credentials_source)
+        except HTTPSRequired:
+            assert False, "Plain HTTP connections to localhost should be allowed."
diff --git a/tests/sap/aibus/dar/client/test_data_manager_client.py b/tests/sap/aibus/dar/client/test_data_manager_client.py
index 3fb57d6..49ef730 100644
--- a/tests/sap/aibus/dar/client/test_data_manager_client.py
+++ b/tests/sap/aibus/dar/client/test_data_manager_client.py
@@ -41,6 +41,14 @@ class AbstractDARClientConstruction:
         with pytest.raises(HTTPSRequired):
             self.clazz(dar_url, source)
 
+    def test_constructor_allows_http_localhost(self):
+        dar_url = "http://localhost:5001/"
+        source = StaticCredentialsSource("1234")
+        try:
+            self.clazz(dar_url, source)
+        except HTTPSRequired:
+            assert False, "Plain-text connection to localhost should be allowed."
+
     def test_create_from_credentials_positional_args(self):
         client = self.clazz.construct_from_credentials(
             self.dar_url, self.clientid, self.clientsecret, self.uaa_url
diff --git a/tests/sap/aibus/dar/util/test_credentials.py b/tests/sap/aibus/dar/util/test_credentials.py
index 8e77af5..35b18a3 100644
--- a/tests/sap/aibus/dar/util/test_credentials.py
+++ b/tests/sap/aibus/dar/util/test_credentials.py
@@ -224,3 +224,13 @@ class TestHTTPSEnforced:
 
         with pytest.raises(HTTPSRequired):
             OnlineCredentialsSource.construct_from_service_key(service_key)
+
+    def test_constructor_allows_http_for_localhost(self):
+        try:
+            OnlineCredentialsSource(
+                url="http://localhost",
+                clientid="a-client-id",
+                clientsecret="a-client-secret",
+            )
+        except HTTPSRequired:
+            assert False, "Plain-text connection to localhost should be allowed."
diff --git a/tests/sap/aibus/dar/util/test_http_transport.py b/tests/sap/aibus/dar/util/test_http_transport.py
index d77f8b6..f3c403b 100644
--- a/tests/sap/aibus/dar/util/test_http_transport.py
+++ b/tests/sap/aibus/dar/util/test_http_transport.py
@@ -21,7 +21,7 @@ class _BaseHTTPSEnforcedTest:
         Non-HTTPS URLs are rejected
         :return:
         """
-        url = "http://localhost/"
+        url = "http://aiservices-dar.cfapps.xxx.hana.ondemand.com/"
         session = self.create_test_object()
         verbs = ["get", "put", "delete", "post", "patch", "request"]
         for verb in verbs: