From 2637e1e42c395f0436d0e784743f30cb22ac6479 Mon Sep 17 00:00:00 2001 From: Clemens Schwaighofer Date: Fri, 24 Oct 2025 19:00:07 +0900 Subject: [PATCH] Tests for requests handling --- src/corelibs/requests_handling/caller.py | 5 +- tests/unit/requests_handling/__init__.py | 3 + .../requests_handling/test_auth_helpers.py | 308 +++++++ tests/unit/requests_handling/test_caller.py | 812 ++++++++++++++++++ 4 files changed, 1126 insertions(+), 2 deletions(-) create mode 100644 tests/unit/requests_handling/__init__.py create mode 100644 tests/unit/requests_handling/test_auth_helpers.py create mode 100644 tests/unit/requests_handling/test_caller.py diff --git a/src/corelibs/requests_handling/caller.py b/src/corelibs/requests_handling/caller.py index e2261b4..e08ea5c 100644 --- a/src/corelibs/requests_handling/caller.py +++ b/src/corelibs/requests_handling/caller.py @@ -18,11 +18,12 @@ class Caller: header: dict[str, str], verify: bool = True, timeout: int = 20, - proxy: dict[str, str] | None = None + proxy: dict[str, str] | None = None, + ca_file: str | None = None ): self.headers = header self.timeout: int = timeout - self.cafile = "/Library/Application Support/Netskope/STAgent/data/nscacert.pem" + self.cafile = ca_file self.verify = verify self.proxy = proxy diff --git a/tests/unit/requests_handling/__init__.py b/tests/unit/requests_handling/__init__.py new file mode 100644 index 0000000..ce0a14c --- /dev/null +++ b/tests/unit/requests_handling/__init__.py @@ -0,0 +1,3 @@ +""" +PyTest: requests_handling tests +""" diff --git a/tests/unit/requests_handling/test_auth_helpers.py b/tests/unit/requests_handling/test_auth_helpers.py new file mode 100644 index 0000000..f5a0d78 --- /dev/null +++ b/tests/unit/requests_handling/test_auth_helpers.py @@ -0,0 +1,308 @@ +""" +PyTest: requests_handling/auth_helpers +""" + +from base64 import b64decode +import pytest +from corelibs.requests_handling.auth_helpers import basic_auth + + +class TestBasicAuth: + """Tests for basic_auth function""" + + def test_basic_credentials(self): + """Test basic auth with simple username and password""" + result = basic_auth("user", "pass") + assert result.startswith("Basic ") + + # Decode and verify the credentials + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "user:pass" + + def test_username_with_special_characters(self): + """Test basic auth with special characters in username""" + result = basic_auth("user@example.com", "password123") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "user@example.com:password123" + + def test_password_with_special_characters(self): + """Test basic auth with special characters in password""" + result = basic_auth("admin", "p@ssw0rd!#$%") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "admin:p@ssw0rd!#$%" + + def test_both_with_special_characters(self): + """Test basic auth with special characters in both username and password""" + result = basic_auth("user@domain.com", "p@ss:w0rd!") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "user@domain.com:p@ss:w0rd!" + + def test_empty_username(self): + """Test basic auth with empty username""" + result = basic_auth("", "password") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == ":password" + + def test_empty_password(self): + """Test basic auth with empty password""" + result = basic_auth("username", "") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "username:" + + def test_both_empty(self): + """Test basic auth with both username and password empty""" + result = basic_auth("", "") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == ":" + + def test_colon_in_username(self): + """Test basic auth with colon in username (edge case)""" + result = basic_auth("user:name", "password") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "user:name:password" + + def test_colon_in_password(self): + """Test basic auth with colon in password""" + result = basic_auth("username", "pass:word") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "username:pass:word" + + def test_unicode_characters(self): + """Test basic auth with unicode characters""" + result = basic_auth("用户", "密码") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "用户:密码" + + def test_long_credentials(self): + """Test basic auth with very long credentials""" + long_user = "a" * 100 + long_pass = "b" * 100 + result = basic_auth(long_user, long_pass) + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == f"{long_user}:{long_pass}" + + def test_whitespace_in_credentials(self): + """Test basic auth with whitespace in credentials""" + result = basic_auth("user name", "pass word") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "user name:pass word" + + def test_newlines_in_credentials(self): + """Test basic auth with newlines in credentials""" + result = basic_auth("user\nname", "pass\nword") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "user\nname:pass\nword" + + def test_return_type(self): + """Test that return type is string""" + result = basic_auth("user", "pass") + assert isinstance(result, str) + + def test_format_consistency(self): + """Test that the format is always 'Basic '""" + result = basic_auth("user", "pass") + parts = result.split(" ") + assert len(parts) == 2 + assert parts[0] == "Basic" + # Verify the second part is valid base64 + try: + b64decode(parts[1]) + except (ValueError, TypeError) as e: + pytest.fail(f"Invalid base64 encoding: {e}") + + def test_known_value(self): + """Test against a known basic auth value""" + # "user:pass" in base64 is "dXNlcjpwYXNz" + result = basic_auth("user", "pass") + assert result == "Basic dXNlcjpwYXNz" + + def test_case_sensitivity(self): + """Test that username and password are case sensitive""" + result1 = basic_auth("User", "Pass") + result2 = basic_auth("user", "pass") + assert result1 != result2 + + def test_ascii_encoding(self): + """Test that the result is ASCII encoded""" + result = basic_auth("user", "pass") + # Should not raise exception + result.encode('ascii') + + +# Parametrized tests +@pytest.mark.parametrize("username,password,expected_decoded", [ + ("admin", "admin123", "admin:admin123"), + ("user@example.com", "password", "user@example.com:password"), + ("test", "test!@#", "test:test!@#"), + ("", "password", ":password"), + ("username", "", "username:"), + ("", "", ":"), + ("user name", "pass word", "user name:pass word"), +]) +def test_basic_auth_parametrized(username: str, password: str, expected_decoded: str): + """Parametrized test for basic_auth""" + result = basic_auth(username, password) + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == expected_decoded + + +@pytest.mark.parametrize("username,password", [ + ("user", "pass"), + ("admin", "secret"), + ("test@example.com", "complex!@#$%^&*()"), + ("a" * 50, "b" * 50), +]) +def test_basic_auth_roundtrip(username: str, password: str): + """Test that we can encode and decode credentials correctly""" + result = basic_auth(username, password) + + # Extract the encoded part + encoded = result.split(" ")[1] + + # Decode and verify + decoded = b64decode(encoded).decode("utf-8") + decoded_username, decoded_password = decoded.split(":", 1) + + assert decoded_username == username + assert decoded_password == password + + +class TestBasicAuthIntegration: + """Integration tests for basic_auth""" + + def test_http_header_format(self): + """Test that the output can be used as HTTP Authorization header""" + auth_header = basic_auth("user", "pass") + + # Simulate HTTP header + headers = {"Authorization": auth_header} + + assert "Authorization" in headers + assert headers["Authorization"].startswith("Basic ") + + def test_multiple_calls_consistency(self): + """Test that multiple calls with same credentials produce same result""" + result1 = basic_auth("user", "pass") + result2 = basic_auth("user", "pass") + result3 = basic_auth("user", "pass") + + assert result1 == result2 == result3 + + def test_different_credentials_different_results(self): + """Test that different credentials produce different results""" + result1 = basic_auth("user1", "pass1") + result2 = basic_auth("user2", "pass2") + result3 = basic_auth("user1", "pass2") + result4 = basic_auth("user2", "pass1") + + results = [result1, result2, result3, result4] + # All should be unique + assert len(results) == len(set(results)) + + +# Edge cases and security considerations +class TestBasicAuthEdgeCases: + """Edge case tests for basic_auth""" + + def test_null_bytes(self): + """Test basic auth with null bytes (security consideration)""" + result = basic_auth("user\x00", "pass\x00") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert "user\x00" in decoded + assert "pass\x00" in decoded + + def test_very_long_username(self): + """Test with extremely long username""" + long_username = "a" * 1000 + result = basic_auth(long_username, "pass") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded.startswith(long_username) + + def test_very_long_password(self): + """Test with extremely long password""" + long_password = "b" * 1000 + result = basic_auth("user", long_password) + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded.endswith(long_password) + + def test_emoji_in_credentials(self): + """Test with emoji characters""" + result = basic_auth("user🔒", "pass🔑") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + assert decoded == "user🔒:pass🔑" + + def test_multiple_colons(self): + """Test with multiple colons in credentials""" + result = basic_auth("user:name:test", "pass:word:test") + assert result.startswith("Basic ") + + encoded = result.split(" ")[1] + decoded = b64decode(encoded).decode("utf-8") + # Only first colon is separator, rest are part of credentials + assert decoded == "user:name:test:pass:word:test" + + def test_base64_special_chars(self): + """Test credentials that might produce base64 with padding""" + # These lengths should produce different padding + result1 = basic_auth("a", "a") + result2 = basic_auth("ab", "ab") + result3 = basic_auth("abc", "abc") + + # All should be valid + for result in [result1, result2, result3]: + assert result.startswith("Basic ") + encoded = result.split(" ")[1] + b64decode(encoded) # Should not raise + + +# __END__ diff --git a/tests/unit/requests_handling/test_caller.py b/tests/unit/requests_handling/test_caller.py new file mode 100644 index 0000000..4e51648 --- /dev/null +++ b/tests/unit/requests_handling/test_caller.py @@ -0,0 +1,812 @@ +""" +PyTest: requests_handling/caller +""" + +from typing import Any +from unittest.mock import Mock, patch +import pytest +import requests +from corelibs.requests_handling.caller import Caller + + +class TestCallerInit: + """Tests for Caller initialization""" + + def test_init_with_required_params_only(self): + """Test Caller initialization with only required parameters""" + header = {"Authorization": "Bearer token"} + caller = Caller(header=header) + + assert caller.headers == header + assert caller.timeout == 20 + assert caller.verify is True + assert caller.proxy is None + assert caller.cafile is None + + def test_init_with_all_params(self): + """Test Caller initialization with all parameters""" + header = {"Authorization": "Bearer token", "Content-Type": "application/json"} + proxy = {"http": "http://proxy.example.com:8080", "https": "https://proxy.example.com:8080"} + caller = Caller(header=header, verify=False, timeout=30, proxy=proxy) + + assert caller.headers == header + assert caller.timeout == 30 + assert caller.verify is False + assert caller.proxy == proxy + + def test_init_with_empty_header(self): + """Test Caller initialization with empty header""" + caller = Caller(header={}) + + assert caller.headers == {} + assert caller.timeout == 20 + + def test_init_custom_timeout(self): + """Test Caller initialization with custom timeout""" + caller = Caller(header={}, timeout=60) + + assert caller.timeout == 60 + + def test_init_verify_false(self): + """Test Caller initialization with verify=False""" + caller = Caller(header={}, verify=False) + + assert caller.verify is False + + def test_init_with_ca_file(self): + """Test Caller initialization with ca_file parameter""" + ca_file_path = "/path/to/ca/cert.pem" + caller = Caller(header={}, ca_file=ca_file_path) + + assert caller.cafile == ca_file_path + + +class TestCallerGet: + """Tests for Caller.get method""" + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_basic(self, mock_get: Mock): + """Test basic GET request""" + mock_response = Mock(spec=requests.Response) + mock_response.status_code = 200 + mock_get.return_value = mock_response + + caller = Caller(header={"Authorization": "Bearer token"}) + response = caller.get("https://api.example.com/data") + + assert response == mock_response + mock_get.assert_called_once_with( + "https://api.example.com/data", + params=None, + headers={"Authorization": "Bearer token"}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_with_params(self, mock_get: Mock): + """Test GET request with query parameters""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}) + params = {"page": 1, "limit": 10} + response = caller.get("https://api.example.com/data", params=params) + + assert response == mock_response + mock_get.assert_called_once_with( + "https://api.example.com/data", + params=params, + headers={}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_with_custom_timeout(self, mock_get: Mock): + """Test GET request uses default timeout from instance""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}, timeout=45) + caller.get("https://api.example.com/data") + + mock_get.assert_called_once() + assert mock_get.call_args[1]["timeout"] == 45 + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_with_verify_false(self, mock_get: Mock): + """Test GET request with verify=False""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}, verify=False) + caller.get("https://api.example.com/data") + + mock_get.assert_called_once() + assert mock_get.call_args[1]["verify"] is False + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_with_proxy(self, mock_get: Mock): + """Test GET request with proxy""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + proxy = {"http": "http://proxy.example.com:8080"} + caller = Caller(header={}, proxy=proxy) + caller.get("https://api.example.com/data") + + mock_get.assert_called_once() + assert mock_get.call_args[1]["proxies"] == proxy + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_invalid_schema_returns_none(self, mock_get: Mock, capsys: Any): + """Test GET request with invalid URL schema returns None""" + mock_get.side_effect = requests.exceptions.InvalidSchema("Invalid URL") + + caller = Caller(header={}) + response = caller.get("invalid://example.com") + + assert response is None + captured = capsys.readouterr() + assert "Invalid URL during 'get'" in captured.out + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_timeout_returns_none(self, mock_get: Mock, capsys: Any): + """Test GET request timeout returns None""" + mock_get.side_effect = requests.exceptions.ReadTimeout("Timeout") + + caller = Caller(header={}) + response = caller.get("https://api.example.com/data") + + assert response is None + captured = capsys.readouterr() + assert "Timeout (20s) during 'get'" in captured.out + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_connection_error_returns_none(self, mock_get: Mock, capsys: Any): + """Test GET request connection error returns None""" + mock_get.side_effect = requests.exceptions.ConnectionError("Connection failed") + + caller = Caller(header={}) + response = caller.get("https://api.example.com/data") + + assert response is None + captured = capsys.readouterr() + assert "Connection error during 'get'" in captured.out + + +class TestCallerPost: + """Tests for Caller.post method""" + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_basic(self, mock_post: Mock): + """Test basic POST request""" + mock_response = Mock(spec=requests.Response) + mock_response.status_code = 201 + mock_post.return_value = mock_response + + caller = Caller(header={"Content-Type": "application/json"}) + data = {"name": "test", "value": 123} + response = caller.post("https://api.example.com/data", data=data) + + assert response == mock_response + mock_post.assert_called_once_with( + "https://api.example.com/data", + params=None, + json=data, + headers={"Content-Type": "application/json"}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_without_data(self, mock_post: Mock): + """Test POST request without data""" + mock_response = Mock(spec=requests.Response) + mock_post.return_value = mock_response + + caller = Caller(header={}) + response = caller.post("https://api.example.com/data") + + assert response == mock_response + mock_post.assert_called_once() + # Data defaults to None, which becomes {} in __call + assert mock_post.call_args[1]["json"] == {} + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_with_params(self, mock_post: Mock): + """Test POST request with query parameters""" + mock_response = Mock(spec=requests.Response) + mock_post.return_value = mock_response + + caller = Caller(header={}) + data = {"key": "value"} + params = {"version": "v1"} + response = caller.post("https://api.example.com/data", data=data, params=params) + + assert response == mock_response + mock_post.assert_called_once() + assert mock_post.call_args[1]["params"] == params + assert mock_post.call_args[1]["json"] == data + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_invalid_schema_returns_none(self, mock_post: Mock, capsys: Any): + """Test POST request with invalid URL schema returns None""" + mock_post.side_effect = requests.exceptions.InvalidSchema("Invalid URL") + + caller = Caller(header={}) + response = caller.post("invalid://example.com", data={"test": "data"}) + + assert response is None + captured = capsys.readouterr() + assert "Invalid URL during 'post'" in captured.out + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_timeout_returns_none(self, mock_post: Mock, capsys: Any): + """Test POST request timeout returns None""" + mock_post.side_effect = requests.exceptions.ReadTimeout("Timeout") + + caller = Caller(header={}) + response = caller.post("https://api.example.com/data", data={"test": "data"}) + + assert response is None + captured = capsys.readouterr() + assert "Timeout (20s) during 'post'" in captured.out + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_connection_error_returns_none(self, mock_post: Mock, capsys: Any): + """Test POST request connection error returns None""" + mock_post.side_effect = requests.exceptions.ConnectionError("Connection failed") + + caller = Caller(header={}) + response = caller.post("https://api.example.com/data", data={"test": "data"}) + + assert response is None + captured = capsys.readouterr() + assert "Connection error during 'post'" in captured.out + + +class TestCallerPut: + """Tests for Caller.put method""" + + @patch('corelibs.requests_handling.caller.requests.put') + def test_put_basic(self, mock_put: Mock): + """Test basic PUT request""" + mock_response = Mock(spec=requests.Response) + mock_response.status_code = 200 + mock_put.return_value = mock_response + + caller = Caller(header={"Content-Type": "application/json"}) + data = {"id": 1, "name": "updated"} + response = caller.put("https://api.example.com/data/1", data=data) + + assert response == mock_response + mock_put.assert_called_once_with( + "https://api.example.com/data/1", + params=None, + json=data, + headers={"Content-Type": "application/json"}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.put') + def test_put_with_params(self, mock_put: Mock): + """Test PUT request with query parameters""" + mock_response = Mock(spec=requests.Response) + mock_put.return_value = mock_response + + caller = Caller(header={}) + data = {"name": "test"} + params = {"force": "true"} + response = caller.put("https://api.example.com/data/1", data=data, params=params) + + assert response == mock_response + mock_put.assert_called_once() + assert mock_put.call_args[1]["params"] == params + + @patch('corelibs.requests_handling.caller.requests.put') + def test_put_timeout_returns_none(self, mock_put: Mock, capsys: Any): + """Test PUT request timeout returns None""" + mock_put.side_effect = requests.exceptions.ReadTimeout("Timeout") + + caller = Caller(header={}) + response = caller.put("https://api.example.com/data/1", data={"test": "data"}) + + assert response is None + captured = capsys.readouterr() + assert "Timeout (20s) during 'put'" in captured.out + + +class TestCallerPatch: + """Tests for Caller.patch method""" + + @patch('corelibs.requests_handling.caller.requests.patch') + def test_patch_basic(self, mock_patch: Mock): + """Test basic PATCH request""" + mock_response = Mock(spec=requests.Response) + mock_response.status_code = 200 + mock_patch.return_value = mock_response + + caller = Caller(header={"Content-Type": "application/json"}) + data = {"status": "active"} + response = caller.patch("https://api.example.com/data/1", data=data) + + assert response == mock_response + mock_patch.assert_called_once_with( + "https://api.example.com/data/1", + params=None, + json=data, + headers={"Content-Type": "application/json"}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.patch') + def test_patch_with_params(self, mock_patch: Mock): + """Test PATCH request with query parameters""" + mock_response = Mock(spec=requests.Response) + mock_patch.return_value = mock_response + + caller = Caller(header={}) + data = {"field": "value"} + params = {"notify": "false"} + response = caller.patch("https://api.example.com/data/1", data=data, params=params) + + assert response == mock_response + mock_patch.assert_called_once() + assert mock_patch.call_args[1]["params"] == params + + @patch('corelibs.requests_handling.caller.requests.patch') + def test_patch_connection_error_returns_none(self, mock_patch: Mock, capsys: Any): + """Test PATCH request connection error returns None""" + mock_patch.side_effect = requests.exceptions.ConnectionError("Connection failed") + + caller = Caller(header={}) + response = caller.patch("https://api.example.com/data/1", data={"test": "data"}) + + assert response is None + captured = capsys.readouterr() + assert "Connection error during 'patch'" in captured.out + + +class TestCallerDelete: + """Tests for Caller.delete method""" + + @patch('corelibs.requests_handling.caller.requests.delete') + def test_delete_basic(self, mock_delete: Mock): + """Test basic DELETE request""" + mock_response = Mock(spec=requests.Response) + mock_response.status_code = 204 + mock_delete.return_value = mock_response + + caller = Caller(header={"Authorization": "Bearer token"}) + response = caller.delete("https://api.example.com/data/1") + + assert response == mock_response + mock_delete.assert_called_once_with( + "https://api.example.com/data/1", + params=None, + headers={"Authorization": "Bearer token"}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.delete') + def test_delete_with_params(self, mock_delete: Mock): + """Test DELETE request with query parameters""" + mock_response = Mock(spec=requests.Response) + mock_delete.return_value = mock_response + + caller = Caller(header={}) + params = {"force": "true"} + response = caller.delete("https://api.example.com/data/1", params=params) + + assert response == mock_response + mock_delete.assert_called_once() + assert mock_delete.call_args[1]["params"] == params + + @patch('corelibs.requests_handling.caller.requests.delete') + def test_delete_invalid_schema_returns_none(self, mock_delete: Mock, capsys: Any): + """Test DELETE request with invalid URL schema returns None""" + mock_delete.side_effect = requests.exceptions.InvalidSchema("Invalid URL") + + caller = Caller(header={}) + response = caller.delete("invalid://example.com/data/1") + + assert response is None + captured = capsys.readouterr() + assert "Invalid URL during 'delete'" in captured.out + + +class TestCallerParametrized: + """Parametrized tests for all HTTP methods""" + + @pytest.mark.parametrize("method,http_method", [ + ("get", "get"), + ("post", "post"), + ("put", "put"), + ("patch", "patch"), + ("delete", "delete"), + ]) + @patch('corelibs.requests_handling.caller.requests') + def test_all_methods_use_correct_headers(self, mock_requests: Mock, method: str, http_method: str): + """Test that all HTTP methods use the headers correctly""" + mock_response = Mock(spec=requests.Response) + mock_http_method = getattr(mock_requests, http_method) + mock_http_method.return_value = mock_response + + headers = {"Authorization": "Bearer token", "X-Custom": "value"} + caller = Caller(header=headers) + + # Call the method + caller_method = getattr(caller, method) + if method in ["get", "delete"]: + caller_method("https://api.example.com/data") + else: + caller_method("https://api.example.com/data", data={"key": "value"}) + + # Verify headers were passed + mock_http_method.assert_called_once() + assert mock_http_method.call_args[1]["headers"] == headers + + @pytest.mark.parametrize("method,http_method", [ + ("get", "get"), + ("post", "post"), + ("put", "put"), + ("patch", "patch"), + ("delete", "delete"), + ]) + @patch('corelibs.requests_handling.caller.requests') + def test_all_methods_use_timeout(self, mock_requests: Mock, method: str, http_method: str): + """Test that all HTTP methods use the timeout correctly""" + mock_response = Mock(spec=requests.Response) + mock_http_method = getattr(mock_requests, http_method) + mock_http_method.return_value = mock_response + + timeout = 45 + caller = Caller(header={}, timeout=timeout) + + # Call the method + caller_method = getattr(caller, method) + if method in ["get", "delete"]: + caller_method("https://api.example.com/data") + else: + caller_method("https://api.example.com/data", data={"key": "value"}) + + # Verify timeout was passed + mock_http_method.assert_called_once() + assert mock_http_method.call_args[1]["timeout"] == timeout + + @pytest.mark.parametrize("exception_class,expected_message", [ + (requests.exceptions.InvalidSchema, "Invalid URL during"), + (requests.exceptions.ReadTimeout, "Timeout"), + (requests.exceptions.ConnectionError, "Connection error during"), + ]) + @patch('corelibs.requests_handling.caller.requests.get') + def test_exception_handling( + self, mock_get: Mock, exception_class: type, expected_message: str, capsys: Any + ): + """Test exception handling for all exception types""" + mock_get.side_effect = exception_class("Test error") + + caller = Caller(header={}) + response = caller.get("https://api.example.com/data") + + assert response is None + captured = capsys.readouterr() + assert expected_message in captured.out + + +class TestCallerIntegration: + """Integration tests for Caller""" + + @patch('corelibs.requests_handling.caller.requests') + def test_multiple_requests_maintain_state(self, mock_requests: Mock): + """Test that multiple requests maintain caller state""" + mock_response = Mock(spec=requests.Response) + mock_requests.get.return_value = mock_response + mock_requests.post.return_value = mock_response + + headers = {"Authorization": "Bearer token"} + caller = Caller(header=headers, timeout=30, verify=False) + + # Make multiple requests + caller.get("https://api.example.com/data1") + caller.post("https://api.example.com/data2", data={"key": "value"}) + + # Verify both used same configuration + assert mock_requests.get.call_args[1]["headers"] == headers + assert mock_requests.get.call_args[1]["timeout"] == 30 + assert mock_requests.get.call_args[1]["verify"] is False + + assert mock_requests.post.call_args[1]["headers"] == headers + assert mock_requests.post.call_args[1]["timeout"] == 30 + assert mock_requests.post.call_args[1]["verify"] is False + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_with_complex_data(self, mock_post: Mock): + """Test POST request with complex nested data""" + mock_response = Mock(spec=requests.Response) + mock_post.return_value = mock_response + + caller = Caller(header={}) + complex_data = { + "user": { + "name": "John Doe", + "email": "john@example.com", + "preferences": { + "notifications": True, + "theme": "dark" + } + }, + "tags": ["important", "urgent"], + "count": 42 + } + response = caller.post("https://api.example.com/users", data=complex_data) + + assert response == mock_response + mock_post.assert_called_once() + assert mock_post.call_args[1]["json"] == complex_data + + @patch('corelibs.requests_handling.caller.requests') + def test_all_http_methods_work_together(self, mock_requests: Mock): + """Test that all HTTP methods can be used with the same Caller instance""" + mock_response = Mock(spec=requests.Response) + for method in ['get', 'post', 'put', 'patch', 'delete']: + getattr(mock_requests, method).return_value = mock_response + + caller = Caller(header={"Authorization": "Bearer token"}) + + # Test all methods + caller.get("https://api.example.com/data") + caller.post("https://api.example.com/data", data={"new": "data"}) + caller.put("https://api.example.com/data/1", data={"updated": "data"}) + caller.patch("https://api.example.com/data/1", data={"field": "value"}) + caller.delete("https://api.example.com/data/1") + + # Verify all were called + mock_requests.get.assert_called_once() + mock_requests.post.assert_called_once() + mock_requests.put.assert_called_once() + mock_requests.patch.assert_called_once() + mock_requests.delete.assert_called_once() + + +class TestCallerEdgeCases: + """Edge case tests for Caller""" + + @patch('corelibs.requests_handling.caller.requests.get') + def test_empty_url(self, mock_get: Mock): + """Test with empty URL""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}) + response = caller.get("") + + assert response == mock_response + mock_get.assert_called_once_with( + "", + params=None, + headers={}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_with_empty_data(self, mock_post: Mock): + """Test POST with explicitly empty data dict""" + mock_response = Mock(spec=requests.Response) + mock_post.return_value = mock_response + + caller = Caller(header={}) + response = caller.post("https://api.example.com/data", data={}) + + assert response == mock_response + mock_post.assert_called_once() + assert mock_post.call_args[1]["json"] == {} + + @patch('corelibs.requests_handling.caller.requests.get') + def test_get_with_empty_params(self, mock_get: Mock): + """Test GET with explicitly empty params dict""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}) + response = caller.get("https://api.example.com/data", params={}) + + assert response == mock_response + mock_get.assert_called_once() + assert mock_get.call_args[1]["params"] == {} + + @patch('corelibs.requests_handling.caller.requests.post') + def test_post_with_none_values_in_data(self, mock_post: Mock): + """Test POST with None values in data""" + mock_response = Mock(spec=requests.Response) + mock_post.return_value = mock_response + + caller = Caller(header={}) + data = {"key1": None, "key2": "value", "key3": None} + response = caller.post("https://api.example.com/data", data=data) + + assert response == mock_response + mock_post.assert_called_once() + assert mock_post.call_args[1]["json"] == data + + @patch('corelibs.requests_handling.caller.requests.get') + def test_very_long_url(self, mock_get: Mock): + """Test with very long URL""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}) + long_url = "https://api.example.com/" + "a" * 1000 + response = caller.get(long_url) + + assert response == mock_response + mock_get.assert_called_once_with( + long_url, + params=None, + headers={}, + timeout=20, + verify=True, + proxies=None + ) + + @patch('corelibs.requests_handling.caller.requests.get') + def test_special_characters_in_url(self, mock_get: Mock): + """Test URL with special characters""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}) + url = "https://api.example.com/data?query=test%20value&id=123" + response = caller.get(url) + + assert response == mock_response + mock_get.assert_called_once_with( + url, + params=None, + headers={}, + timeout=20, + verify=True, + proxies=None + ) + + def test_timeout_zero(self): + """Test Caller with timeout of 0""" + caller = Caller(header={}, timeout=0) + assert caller.timeout == 0 + + def test_negative_timeout(self): + """Test Caller with negative timeout""" + caller = Caller(header={}, timeout=-1) + assert caller.timeout == -1 + + @patch('corelibs.requests_handling.caller.requests.get') + def test_unicode_in_headers(self, mock_get: Mock): + """Test headers with unicode characters""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + headers = {"X-Custom": "测试", "Authorization": "Bearer token"} + caller = Caller(header=headers) + response = caller.get("https://api.example.com/data") + + assert response == mock_response + mock_get.assert_called_once() + assert mock_get.call_args[1]["headers"] == headers + + @patch('corelibs.requests_handling.caller.requests.post') + def test_unicode_in_data(self, mock_post: Mock): + """Test data with unicode characters""" + mock_response = Mock(spec=requests.Response) + mock_post.return_value = mock_response + + caller = Caller(header={}) + data = {"name": "用户", "message": "こんにちは", "emoji": "🚀"} + response = caller.post("https://api.example.com/data", data=data) + + assert response == mock_response + mock_post.assert_called_once() + assert mock_post.call_args[1]["json"] == data + + +class TestCallerProxyHandling: + """Tests for proxy handling""" + + @patch('corelibs.requests_handling.caller.requests.get') + def test_proxy_configuration(self, mock_get: Mock): + """Test that proxy configuration is passed to requests""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + proxy = { + "http": "http://proxy.example.com:8080", + "https": "https://proxy.example.com:8080" + } + caller = Caller(header={}, proxy=proxy) + caller.get("https://api.example.com/data") + + mock_get.assert_called_once() + assert mock_get.call_args[1]["proxies"] == proxy + + @patch('corelibs.requests_handling.caller.requests.post') + def test_proxy_with_auth(self, mock_post: Mock): + """Test proxy with authentication""" + mock_response = Mock(spec=requests.Response) + mock_post.return_value = mock_response + + proxy = { + "http": "http://user:pass@proxy.example.com:8080", + "https": "https://user:pass@proxy.example.com:8080" + } + caller = Caller(header={}, proxy=proxy) + caller.post("https://api.example.com/data", data={"test": "data"}) + + mock_post.assert_called_once() + assert mock_post.call_args[1]["proxies"] == proxy + + +class TestCallerTimeoutHandling: + """Tests for timeout parameter handling""" + + @patch('corelibs.requests_handling.caller.requests.get') + def test_timeout_parameter_none_uses_default(self, mock_get: Mock): + """Test that None timeout uses the instance default""" + mock_response = Mock(spec=requests.Response) + mock_get.return_value = mock_response + + caller = Caller(header={}, timeout=30) + # The private __timeout method is called internally + caller.get("https://api.example.com/data") + + mock_get.assert_called_once() + assert mock_get.call_args[1]["timeout"] == 30 + + +class TestCallerResponseHandling: + """Tests for response handling""" + + @patch('corelibs.requests_handling.caller.requests.get') + def test_response_object_returned_correctly(self, mock_get: Mock): + """Test that response object is returned correctly""" + mock_response = Mock(spec=requests.Response) + mock_response.status_code = 200 + mock_response.text = "Success" + mock_response.json.return_value = {"status": "ok"} + mock_get.return_value = mock_response + + caller = Caller(header={}) + response = caller.get("https://api.example.com/data") + + assert response is not None + assert response.status_code == 200 + assert response.text == "Success" + assert response.json() == {"status": "ok"} + + @patch('corelibs.requests_handling.caller.requests.get') + def test_response_with_different_status_codes(self, mock_get: Mock): + """Test response handling with different status codes""" + for status_code in [200, 201, 204, 400, 401, 404, 500]: + mock_response = Mock(spec=requests.Response) + mock_response.status_code = status_code + mock_get.return_value = mock_response + + caller = Caller(header={}) + response = caller.get("https://api.example.com/data") + + assert response is not None + assert response.status_code == status_code + + +# __END__