From 12aa77dd5bf5b6515007372ad9d2386fe6bb3761 Mon Sep 17 00:00:00 2001 From: Mathieu Leplatre Date: Thu, 28 Nov 2024 13:50:53 +0100 Subject: [PATCH] Add 'dry_mode' parameter --- README.rst | 9 +++++++++ src/kinto_http/batch.py | 4 ++++ src/kinto_http/client.py | 8 ++++++-- src/kinto_http/session.py | 27 +++++++++++++++++++++++--- tests/conftest.py | 40 +++++++++++++++++++++------------------ tests/test_functional.py | 13 +++++++++++++ tests/test_session.py | 12 ++++++++++++ 7 files changed, 90 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 4f05405..eb9838c 100644 --- a/README.rst +++ b/README.rst @@ -80,6 +80,15 @@ An asynchronous client is also available. It has all the same endpoints as the s info = await client.server_info() assert 'schema' in info['capabilities'], "Server doesn't support schema validation." + +Dry Mode +-------- + +The ``dry_mode`` parameter can be set to simulate requests without actually sending them over the network. +When enabled, dry mode ensures that no external calls are made, making it useful for testing or debugging. +Instead of performing real HTTP operations, the client logs the requests with ``DEBUG`` level. + + Using a Bearer access token to authenticate (OpenID) ---------------------------------------------------- diff --git a/src/kinto_http/batch.py b/src/kinto_http/batch.py index 05aa693..a1a5c63 100644 --- a/src/kinto_http/batch.py +++ b/src/kinto_http/batch.py @@ -79,6 +79,10 @@ def send(self): method="POST", endpoint=self.endpoints.get("batch"), payload={"requests": chunk} ) resp, headers = self.session.request(**kwargs) + if self.session.dry_mode: + resp.setdefault( + "responses", [{"status": 200, "body": {}} for i in range(len(chunk))] + ) for i, response in enumerate(resp["responses"]): status_code = response["status"] diff --git a/src/kinto_http/client.py b/src/kinto_http/client.py index cb5ccce..86f6a6d 100644 --- a/src/kinto_http/client.py +++ b/src/kinto_http/client.py @@ -46,6 +46,7 @@ def __init__( timeout=None, ignore_batch_4xx=False, headers=None, + dry_mode=False, ): self.endpoints = Endpoints() @@ -63,6 +64,7 @@ def __init__( retry_after=retry_after, timeout=timeout, headers=headers, + dry_mode=dry_mode, ) self.session = create_session(**session_kwargs) self.bucket_name = bucket @@ -88,9 +90,11 @@ def clone(self, **kwargs): def batch(self, **kwargs): if self._server_settings is None: resp, _ = self.session.request("GET", self._get_endpoint("root")) - self._server_settings = resp["settings"] + self._server_settings = resp["settings"] if not self.session.dry_mode else {} - batch_max_requests = self._server_settings["batch_max_requests"] + batch_max_requests = ( + self._server_settings["batch_max_requests"] if not self.session.dry_mode else 999999 + ) batch_session = BatchSession( self, batch_max_requests=batch_max_requests, ignore_4xx_errors=self._ignore_batch_4xx ) diff --git a/src/kinto_http/session.py b/src/kinto_http/session.py index fcae98d..3726f6c 100644 --- a/src/kinto_http/session.py +++ b/src/kinto_http/session.py @@ -1,9 +1,11 @@ import json +import logging import time import warnings -from urllib.parse import urlparse +from urllib.parse import urlencode, urlparse import requests +from urllib3.response import HTTPResponse import kinto_http from kinto_http import utils @@ -11,6 +13,9 @@ from kinto_http.exceptions import BackoffException, KintoException +logger = logging.getLogger(__name__) + + def create_session(server_url=None, auth=None, session=None, **kwargs): """Returns a session from the passed arguments. @@ -55,7 +60,14 @@ class Session(object): """Handles all the interactions with the network.""" def __init__( - self, server_url, auth=None, timeout=False, headers=None, retry=0, retry_after=None + self, + server_url, + auth=None, + timeout=False, + headers=None, + retry=0, + retry_after=None, + dry_mode=False, ): self.backoff = None self.server_url = server_url @@ -64,6 +76,7 @@ def __init__( self.retry_after = retry_after self.timeout = timeout self.headers = headers or {} + self.dry_mode = dry_mode def request(self, method, endpoint, data=None, permissions=None, payload=None, **kwargs): current_time = time.time() @@ -123,7 +136,15 @@ def request(self, method, endpoint, data=None, permissions=None, payload=None, * retry = self.nb_retry while retry >= 0: - resp = requests.request(method, actual_url, **kwargs) + if self.dry_mode: + qs = ("?" + urlencode(kwargs["params"])) if "params" in kwargs else "" + logger.debug(f"(dry mode) {method} {actual_url}{qs}") + resp = HTTPResponse( + status=200, headers={"Content-Type": "application/json"}, body=b"{}" + ) + resp.status_code = resp.status + else: + resp = requests.request(method, actual_url, **kwargs) if "Alert" in resp.headers: warnings.warn(resp.headers["Alert"], DeprecationWarning) diff --git a/tests/conftest.py b/tests/conftest.py index 3b9162e..f0ef6d0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,34 +16,37 @@ @pytest.fixture -def async_client_setup(mocker: MockerFixture) -> AsyncClient: +def mocked_session(mocker: MockerFixture): session = mocker.MagicMock() - mock_response(session) - client = AsyncClient(session=session, bucket="mybucket") + session.dry_mode = False + return session + + +@pytest.fixture +def async_client_setup(mocked_session, mocker: MockerFixture) -> AsyncClient: + mock_response(mocked_session) + client = AsyncClient(session=mocked_session, bucket="mybucket") return client @pytest.fixture -def client_setup(mocker: MockerFixture) -> Client: - session = mocker.MagicMock() - mock_response(session) - client = Client(session=session, bucket="mybucket") +def client_setup(mocked_session, mocker: MockerFixture) -> Client: + mock_response(mocked_session) + client = Client(session=mocked_session, bucket="mybucket") return client @pytest.fixture -def record_async_setup(mocker: MockerFixture) -> AsyncClient: - session = mocker.MagicMock() - session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count) - client = AsyncClient(session=session, bucket="mybucket", collection="mycollection") +def record_async_setup(mocked_session, mocker: MockerFixture) -> AsyncClient: + mocked_session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count) + client = AsyncClient(session=mocked_session, bucket="mybucket", collection="mycollection") return client @pytest.fixture -def record_setup(mocker: MockerFixture) -> Client: - session = mocker.MagicMock() - session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count) - client = Client(session=session, bucket="mybucket", collection="mycollection") +def record_setup(mocked_session, mocker: MockerFixture) -> Client: + mocked_session.request.return_value = (mocker.sentinel.response, mocker.sentinel.count) + client = Client(session=mocked_session, bucket="mybucket", collection="mycollection") return client @@ -87,10 +90,11 @@ def endpoints_setup() -> Tuple[Endpoints, Dict]: @pytest.fixture -def batch_setup(mocker: MockerFixture) -> Client: - client = mocker.MagicMock() +def batch_setup(mocked_session, mocker: MockerFixture) -> Client: mocker.sentinel.resp = {"responses": []} - client.session.request.return_value = (mocker.sentinel.resp, mocker.sentinel.headers) + mocked_session.request.return_value = (mocker.sentinel.resp, mocker.sentinel.headers) + client = mocker.MagicMock() + client.session = mocked_session return client diff --git a/tests/test_functional.py b/tests/test_functional.py index ca5139c..c9d7434 100644 --- a/tests/test_functional.py +++ b/tests/test_functional.py @@ -590,3 +590,16 @@ def test_get_permissions(functional_setup): perms_by_uri = {p["uri"]: p for p in perms} assert set(perms_by_uri["/accounts/user"]["permissions"]) == {"read", "write"} + + +def test_dry_mode(functional_setup): + client = functional_setup.clone(server_url="http://not-a-valid-domain:42", dry_mode=True) + + with client.batch() as batch: + batch.create_bucket() + batch.create_collection(id="cid") + + r1, r2 = batch.results() + + assert r1 == {} + assert r2 == {} diff --git a/tests/test_session.py b/tests/test_session.py index da24c5c..8827bda 100644 --- a/tests/test_session.py +++ b/tests/test_session.py @@ -1,3 +1,4 @@ +import logging import sys import time import warnings @@ -545,3 +546,14 @@ def test_next_request_without_the_header_clear_the_backoff( time.sleep(1) # Spend the backoff session.request("get", "/test") # The second call reset the backoff assert session.backoff is None + + +def test_dry_mode_logs_debug(caplog): + caplog.set_level(logging.DEBUG) + + session = Session(server_url="https://foo:42", dry_mode=True) + body, headers = session.request("GET", "/test", params={"_since": "333"}) + + assert body == {} + assert headers == {"Content-Type": "application/json"} + assert caplog.messages == ["(dry mode) GET https://foo:42/test?_since=333"]