diff --git a/.gitignore b/.gitignore index c1eb3ea..6b817e9 100755 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,9 @@ nosetests.xml #PyPi setup.cfg .pypirc + +# macOS +.DS_Store + +# Test artifacts +pytest.xml diff --git a/examples/authentication_example.py b/examples/authentication_example.py new file mode 100644 index 0000000..9f7d3ec --- /dev/null +++ b/examples/authentication_example.py @@ -0,0 +1,72 @@ +""" +Example of using Personal Access Token authentication with MediaWikiAPI. + +Wikimedia API supports Personal Access Tokens for authentication. +See: https://api.wikimedia.org/wiki/Authentication + +This example shows how to configure MediaWikiAPI with: +1. Personal Access Token authentication +2. Custom HTTP headers +""" + +from mediawikiapi import MediaWikiAPI +from mediawikiapi.config import Config + +# Example 1: Using Personal Access Token +# Get your token from: https://api.wikimedia.org/ +access_token = "your_personal_access_token_here" + +config_with_token = Config(access_token=access_token) +api_with_auth = MediaWikiAPI(config=config_with_token) + +# Now all requests will include: Authorization: Bearer +# page = api_with_auth.page("Python (programming language)") +# print(page.summary) + + +# Example 2: Using custom headers +custom_headers = { + "X-Custom-Header": "my-value", + "Accept-Language": "en-US", +} + +config_with_headers = Config(custom_headers=custom_headers) +api_with_headers = MediaWikiAPI(config=config_with_headers) + + +# Example 3: Combining access token with custom headers +config_combined = Config( + access_token=access_token, + custom_headers={ + "X-Application-Name": "MyWikipediaBot", + "X-Application-Version": "1.0.0", + }, +) +api_combined = MediaWikiAPI(config=config_combined) + + +# Example 4: Override default User-Agent +config_custom_ua = Config( + custom_headers={"User-Agent": "MyCustomBot/1.0 (contact@example.com)"} +) +api_custom_ua = MediaWikiAPI(config=config_custom_ua) + + +# Example 5: Using with Wikimedia API (not Wikipedia) +# For authenticated requests to Wikimedia API endpoints +config_wikimedia = Config( + mediawiki_url="https://api.wikimedia.org/core/v1/wikipedia/en/", + access_token=access_token, +) +# Note: Wikimedia API has different endpoints and response formats +# This is just an example of how to configure authentication + + +if __name__ == "__main__": + print("Authentication examples loaded successfully!") + print("\nConfiguration with access token:") + print(f" Headers: {config_with_token.get_headers()}") + print("\nConfiguration with custom headers:") + print(f" Headers: {config_with_headers.get_headers()}") + print("\nCombined configuration:") + print(f" Headers: {config_combined.get_headers()}") diff --git a/mediawikiapi/config.py b/mediawikiapi/config.py index 71258ce..0a5cf02 100755 --- a/mediawikiapi/config.py +++ b/mediawikiapi/config.py @@ -1,11 +1,11 @@ from datetime import timedelta -from typing import Union, Optional +from typing import Union, Optional, Dict from .language import Language class Config(object): """ - Contains global configuration + Contains global configuration for MediaWiki API requests. """ DEFAULT_TIMEOUT = 3.0 @@ -22,7 +22,23 @@ def __init__( timeout: Optional[float] = None, rate_limit: Optional[Union[int, timedelta]] = None, mediawiki_url: Optional[str] = None, + access_token: Optional[str] = None, + custom_headers: Optional[Dict[str, str]] = None, ): + """ + Initialize MediaWiki API configuration. + + Args: + language: Language code for Wikipedia (e.g., 'en', 'fr'). Defaults to 'en'. + user_agent: Custom User-Agent string for requests. Defaults to library identifier. + timeout: Request timeout in seconds. Defaults to 3.0. + rate_limit: Minimum time between requests (int as milliseconds or timedelta). + mediawiki_url: Custom MediaWiki API URL. Defaults to Wikipedia URL pattern. + access_token: Personal Access Token for Wikimedia API authentication. + See https://api.wikimedia.org/wiki/Authentication + custom_headers: Additional HTTP headers to include in all requests. + Can override default headers including User-Agent. + """ if language is not None: self.__lang = Language(language) else: @@ -33,6 +49,8 @@ def __init__( self.timeout: float = timeout or self.DEFAULT_TIMEOUT self.user_agent: str = user_agent or self.DEFAULT_USER_AGENT self.mediawiki_url: str = mediawiki_url or self.API_URL + self.access_token: Optional[str] = access_token + self.custom_headers: Dict[str, str] = custom_headers or {} @classmethod def donate_url(cls) -> str: @@ -95,3 +113,23 @@ def rate_limit(self, rate_limit: Optional[Union[int, timedelta]] = None) -> None self.__rate_limit = rate_limit else: self.__rate_limit = timedelta(milliseconds=rate_limit) + + def get_headers(self) -> Dict[str, str]: + """ + Get HTTP headers for API requests including authentication. + + Returns: + Dictionary of HTTP headers including User-Agent, Authorization (if access_token is set), + and any custom headers. + """ + headers = {"User-Agent": self.user_agent} + + # Add Personal Access Token authentication if provided + # https://api.wikimedia.org/wiki/Authentication + if self.access_token: + headers["Authorization"] = f"Bearer {self.access_token}" + + # Merge custom headers (custom headers can override defaults) + headers.update(self.custom_headers) + + return headers diff --git a/mediawikiapi/requestsession.py b/mediawikiapi/requestsession.py index 8660168..a7a8d96 100755 --- a/mediawikiapi/requestsession.py +++ b/mediawikiapi/requestsession.py @@ -51,7 +51,7 @@ def request( if "action" not in params: params["action"] = "query" - headers = {"User-Agent": config.user_agent} + headers = config.get_headers() if ( self.__rate_limit_last_call diff --git a/poetry.lock b/poetry.lock index c6f0809..8779c65 100644 --- a/poetry.lock +++ b/poetry.lock @@ -119,7 +119,7 @@ version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, @@ -131,7 +131,7 @@ version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, @@ -1062,7 +1062,7 @@ version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, @@ -1078,6 +1078,26 @@ urllib3 = ">=1.21.1,<3" socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] +[[package]] +name = "responses" +version = "0.25.8" +description = "A utility library for mocking out the `requests` Python library." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c"}, + {file = "responses-0.25.8.tar.gz", hash = "sha256:9374d047a575c8f781b94454db5cab590b6029505f488d12899ddb10a4af1cf4"}, +] + +[package.dependencies] +pyyaml = "*" +requests = ">=2.30.0,<3.0" +urllib3 = ">=1.25.10,<3.0" + +[package.extras] +tests = ["coverage (>=6.0.0)", "flake8", "mypy", "pytest (>=7.0.0)", "pytest-asyncio", "pytest-cov", "pytest-httpserver", "tomli ; python_version < \"3.11\"", "tomli-w", "types-PyYAML", "types-requests"] + [[package]] name = "six" version = "1.17.0" @@ -1618,4 +1638,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.1" python-versions = "^3.9" -content-hash = "c1696e0e74e27ccbe1e9dd56e79a3314d8f5c60afaa386bd00ff996f44571a4a" +content-hash = "3e9af2fec8123964e816f02083c2932b5140b4c3eb87d0f5e7a948999fdd2da0" diff --git a/pyproject.toml b/pyproject.toml index e14f4f1..bda3bf4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,6 +23,7 @@ flake8 = "^7.1.2" pytest = "^8.3.5" pytest-cov = "^6.0.0" pytest-vcr = "^1.0.2" +responses = "^0.25.0" typing-extensions = "^4.12.2" types-beautifulsoup4 = "^4.12.0.20250204" types-requests = "^2.32.0.20250306" diff --git a/tests/manual_auth_test.py b/tests/manual_auth_test.py new file mode 100644 index 0000000..d30ef05 --- /dev/null +++ b/tests/manual_auth_test.py @@ -0,0 +1,210 @@ +""" +Manual testing script for Personal Access Token authentication. + +This script helps you verify that authentication headers are being sent correctly +to the MediaWiki API. + +USAGE: + 1. Get a Personal Access Token from https://api.wikimedia.org/ + 2. Set the token: export WIKIMEDIA_ACCESS_TOKEN="your_token_here" + 3. Run: python tests/manual_auth_test.py + +This script will: +- Show what headers are being sent +- Make a test request to Wikipedia +- Display the response to verify everything works +""" + +import os +import sys +from pprint import pprint + +# Add parent directory to path for imports +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))) + +from mediawikiapi import MediaWikiAPI +from mediawikiapi.config import Config + + +def print_section(title: str) -> None: + """Print a formatted section header""" + print("\n" + "=" * 70) + print(f" {title}") + print("=" * 70) + + +def test_without_auth() -> None: + """Test API requests without authentication""" + print_section("Test 1: Without Authentication") + + config = Config() + api = MediaWikiAPI(config=config) + + print("\nHeaders being sent:") + pprint(config.get_headers()) + + print("\nMaking request to Wikipedia...") + try: + results = api.search("Python programming", results=3) + print(f"\n✅ Success! Found {len(results)} results:") + for i, result in enumerate(results, 1): + print(f" {i}. {result}") + except Exception as e: + print(f"\n❌ Error: {e}") + + +def test_with_auth() -> None: + """Test API requests with Personal Access Token""" + print_section("Test 2: With Personal Access Token") + + # Get token from environment variable + access_token = os.environ.get("WIKIMEDIA_ACCESS_TOKEN") + + if not access_token: + print("\n⚠️ No access token found.") + print("Set it with: export WIKIMEDIA_ACCESS_TOKEN='your_token_here'") + print("Get a token from: https://api.wikimedia.org/") + return + + config = Config(access_token=access_token) + api = MediaWikiAPI(config=config) + + print("\nHeaders being sent:") + headers = config.get_headers() + # Mask the token for security + if "Authorization" in headers: + token_value = headers["Authorization"] + masked_token = ( + token_value[:20] + "..." + token_value[-10:] + if len(token_value) > 30 + else "***MASKED***" + ) + headers_display = headers.copy() + headers_display["Authorization"] = masked_token + pprint(headers_display) + else: + pprint(headers) + + print("\nMaking authenticated request to Wikipedia...") + try: + results = api.search("Python programming", results=3) + print(f"\n✅ Success! Found {len(results)} results:") + for i, result in enumerate(results, 1): + print(f" {i}. {result}") + except Exception as e: + print(f"\n❌ Error: {e}") + + +def test_with_custom_headers() -> None: + """Test API requests with custom headers""" + print_section("Test 3: With Custom Headers") + + custom_headers = { + "X-Application-Name": "MediaWikiAPI-Test", + "X-Application-Version": "1.0.0", + } + + config = Config(custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + print("\nHeaders being sent:") + pprint(config.get_headers()) + + print("\nMaking request with custom headers...") + try: + results = api.search("Python programming", results=3) + print(f"\n✅ Success! Found {len(results)} results:") + for i, result in enumerate(results, 1): + print(f" {i}. {result}") + except Exception as e: + print(f"\n❌ Error: {e}") + + +def test_custom_user_agent() -> None: + """Test API requests with custom User-Agent""" + print_section("Test 4: With Custom User-Agent") + + custom_headers = {"User-Agent": "MyTestBot/1.0 (testing@example.com)"} + + config = Config(custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + print("\nHeaders being sent:") + pprint(config.get_headers()) + + print("\nMaking request with custom User-Agent...") + try: + results = api.search("Python programming", results=3) + print(f"\n✅ Success! Found {len(results)} results:") + for i, result in enumerate(results, 1): + print(f" {i}. {result}") + except Exception as e: + print(f"\n❌ Error: {e}") + + +def test_combined() -> None: + """Test with both authentication and custom headers""" + print_section("Test 5: Combined Authentication + Custom Headers") + + access_token = os.environ.get("WIKIMEDIA_ACCESS_TOKEN") + + if not access_token: + print("\n⚠️ Skipping - no access token found.") + return + + custom_headers = { + "X-Application-Name": "MediaWikiAPI-Test", + "X-Request-ID": "test-12345", + } + + config = Config(access_token=access_token, custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + print("\nHeaders being sent:") + headers = config.get_headers() + # Mask the token + if "Authorization" in headers: + token_value = headers["Authorization"] + masked_token = ( + token_value[:20] + "..." + token_value[-10:] + if len(token_value) > 30 + else "***MASKED***" + ) + headers_display = headers.copy() + headers_display["Authorization"] = masked_token + pprint(headers_display) + else: + pprint(headers) + + print("\nMaking combined request...") + try: + results = api.search("Python programming", results=3) + print(f"\n✅ Success! Found {len(results)} results:") + for i, result in enumerate(results, 1): + print(f" {i}. {result}") + except Exception as e: + print(f"\n❌ Error: {e}") + + +def main() -> None: + """Run all tests""" + print("\n" + "=" * 70) + print(" MediaWikiAPI Authentication Testing") + print("=" * 70) + + # Run all tests + test_without_auth() + test_with_auth() + test_with_custom_headers() + test_custom_user_agent() + test_combined() + + print_section("Testing Complete") + print("\n✅ All tests completed successfully!") + print("\nNote: Wikipedia API works without authentication. To test authenticated") + print(" requests, use a Wikimedia API endpoint that requires authentication.") + print(" See: https://api.wikimedia.org/wiki/Authentication\n") + + +if __name__ == "__main__": + main() diff --git a/tests/test_auth_integration.py b/tests/test_auth_integration.py new file mode 100644 index 0000000..db3ae5f --- /dev/null +++ b/tests/test_auth_integration.py @@ -0,0 +1,229 @@ +""" +Integration tests for authentication using responses library to intercept HTTP calls. + +These tests verify that headers are correctly sent in real HTTP requests. +""" + +import unittest +from unittest.mock import patch +import responses +from responses import matchers +from mediawikiapi import MediaWikiAPI +from mediawikiapi.config import Config +from mediawikiapi.language import Language + + +class TestAuthenticationIntegration(unittest.TestCase): + """Integration tests that intercept actual HTTP requests""" + + def setUp(self) -> None: + """Set mock Language cache before each test to prevent real API calls""" + self._original_languages = Language.predefined_languages + Language.predefined_languages = { + "en": "English", + "es": "Spanish", + "fr": "French", + "ru": "Russian", + "uk": "Ukrainian", + } + + def tearDown(self) -> None: + """Restore original Language cache after each test""" + Language.predefined_languages = self._original_languages + + @responses.activate + def test_auth_header_sent_in_real_request(self) -> None: + """Verify Authorization header is sent in actual HTTP request""" + access_token = "test_token_12345" + config = Config(access_token=access_token) + api = MediaWikiAPI(config=config) + + responses.add( + responses.GET, + "https://en.wikipedia.org/w/api.php", + json={"query": {"search": [{"title": "AuthTokenTest", "pageid": 123}]}}, + match=[ + matchers.query_param_matcher( + { + "list": "search", + "srlimit": "10", + "limit": "10", + "srsearch": "AuthTokenTest", + "format": "json", + "action": "query", + } + ) + ], + status=200, + ) + + api.search("AuthTokenTest") + + assert len(responses.calls) == 1 + request = responses.calls[0].request + assert "Authorization" in request.headers + assert request.headers["Authorization"] == f"Bearer {access_token}" + + @responses.activate + def test_custom_headers_sent_in_real_request(self) -> None: + """Verify custom headers are sent in actual HTTP request""" + custom_headers = {"X-Custom-App": "TestApp", "X-Version": "1.0.0"} + config = Config(custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + responses.add( + responses.GET, + "https://en.wikipedia.org/w/api.php", + json={"query": {"search": [{"title": "CustomHeadersTest", "pageid": 124}]}}, + match=[ + matchers.query_param_matcher( + { + "list": "search", + "srlimit": "10", + "limit": "10", + "srsearch": "CustomHeadersTest", + "format": "json", + "action": "query", + } + ) + ], + status=200, + ) + + api.search("CustomHeadersTest") + + assert len(responses.calls) == 1 + request = responses.calls[0].request + assert "X-Custom-App" in request.headers + assert request.headers["X-Custom-App"] == "TestApp" + assert "X-Version" in request.headers + assert request.headers["X-Version"] == "1.0.0" + + @responses.activate + def test_user_agent_override(self) -> None: + """Verify custom User-Agent overrides default""" + custom_user_agent = "MyBot/1.0 (test@example.com)" + custom_headers = {"User-Agent": custom_user_agent} + config = Config(custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + responses.add( + responses.GET, + "https://en.wikipedia.org/w/api.php", + json={"query": {"search": [{"title": "UserAgentTest", "pageid": 125}]}}, + match=[ + matchers.query_param_matcher( + { + "list": "search", + "srlimit": "10", + "limit": "10", + "srsearch": "UserAgentTest", + "format": "json", + "action": "query", + } + ) + ], + status=200, + ) + + api.search("UserAgentTest") + + assert len(responses.calls) == 1 + request = responses.calls[0].request + assert request.headers["User-Agent"] == custom_user_agent + + @responses.activate + def test_no_auth_without_token(self) -> None: + """Verify no Authorization header when access_token not provided""" + config = Config() + api = MediaWikiAPI(config=config) + + responses.add( + responses.GET, + "https://en.wikipedia.org/w/api.php", + json={"query": {"search": [{"title": "NoAuthTest", "pageid": 126}]}}, + match=[ + matchers.query_param_matcher( + { + "list": "search", + "srlimit": "10", + "limit": "10", + "srsearch": "NoAuthTest", + "format": "json", + "action": "query", + } + ) + ], + status=200, + ) + + api.search("NoAuthTest") + + assert len(responses.calls) == 1 + request = responses.calls[0].request + assert "Authorization" not in request.headers + assert "User-Agent" in request.headers + + @responses.activate + def test_headers_in_continuation_requests(self) -> None: + """Verify headers are included in continuation requests too""" + access_token = "test_token_12345" + config = Config(access_token=access_token) + api = MediaWikiAPI(config=config) + + responses.add( + responses.GET, + "https://en.wikipedia.org/w/api.php", + json={ + "continue": {"continue": "-||", "sroffset": 10}, + "query": {"search": [{"title": "ContinuationTest 1", "pageid": 127}]}, + }, + match=[ + matchers.query_param_matcher( + { + "list": "search", + "srlimit": "10", + "limit": "10", + "srsearch": "ContinuationTest", + "format": "json", + "action": "query", + } + ) + ], + status=200, + ) + + responses.add( + responses.GET, + "https://en.wikipedia.org/w/api.php", + json={ + "query": {"search": [{"title": "ContinuationTest 2", "pageid": 128}]} + }, + match=[ + matchers.query_param_matcher( + { + "list": "search", + "srlimit": "10", + "limit": "10", + "srsearch": "ContinuationTest", + "sroffset": "10", + "continue": "-||", + "format": "json", + "action": "query", + } + ) + ], + status=200, + ) + + api.search("ContinuationTest", follow_continue=True) + + assert len(responses.calls) == 2 + for call in responses.calls: + request = call.request + assert "Authorization" in request.headers + assert request.headers["Authorization"] == f"Bearer {access_token}" + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_authentication.py b/tests/test_authentication.py new file mode 100644 index 0000000..4ccfd90 --- /dev/null +++ b/tests/test_authentication.py @@ -0,0 +1,196 @@ +""" +Tests for Personal Access Token authentication and custom headers. + +These tests verify that authentication headers are properly included in API requests. +""" + +import unittest +from typing import Any +from unittest.mock import Mock, patch +from mediawikiapi import MediaWikiAPI +from mediawikiapi.config import Config +from mediawikiapi.language import Language + + +class TestAuthentication(unittest.TestCase): + """Test authentication header handling in API requests""" + + def setUp(self) -> None: + """Set mock Language cache before each test and clear memoization""" + self._original_languages = Language.predefined_languages + Language.predefined_languages = { + "en": "English", + "es": "Spanish", + "fr": "French", + "ru": "Russian", + "uk": "Ukrainian", + } + # Clear memoization cache to ensure tests are isolated + if hasattr(MediaWikiAPI, "_memoized_functions"): + for func in MediaWikiAPI._memoized_functions.values(): + if hasattr(func, "cache"): + func.cache.clear() + + def tearDown(self) -> None: + """Restore original Language cache after each test""" + Language.predefined_languages = self._original_languages + + @patch("mediawikiapi.requestsession.requests.Session") + def test_access_token_sent_in_request(self, mock_session_class: Any) -> None: + """Verify that access token is included in Authorization header""" + # Setup mock session + mock_session = Mock() + mock_session_class.return_value = mock_session + + # Mock the response + mock_response = Mock() + mock_response.json.return_value = { + "query": {"search": [{"title": "Test Page", "pageid": 123}]} + } + mock_session.get.return_value = mock_response + + # Create config and API + access_token = "test_token_12345" + config = Config(access_token=access_token) + api = MediaWikiAPI(config=config) + + # Make a request with unique query + api.search("access token test") + + # Verify headers were passed + mock_session.get.assert_called() + call_args = mock_session.get.call_args + headers = call_args[1]["headers"] + + self.assertIn("Authorization", headers) + self.assertEqual(headers["Authorization"], f"Bearer {access_token}") + + @patch("mediawikiapi.requestsession.requests.Session") + def test_custom_headers_sent_in_request(self, mock_session_class: Any) -> None: + """Verify that custom headers are included in requests""" + # Setup mock session + mock_session = Mock() + mock_session_class.return_value = mock_session + mock_response = Mock() + mock_response.json.return_value = { + "query": {"search": [{"title": "Test Page", "pageid": 123}]} + } + mock_session.get.return_value = mock_response + + # Create config with custom headers + custom_headers = { + "X-Custom-Header": "custom_value", + "X-Application-Name": "TestApp", + } + config = Config(custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + # Make a request with unique query + api.search("custom headers test") + + # Verify custom headers were passed + mock_session.get.assert_called() + call_args = mock_session.get.call_args + headers = call_args[1]["headers"] + + self.assertIn("X-Custom-Header", headers) + self.assertEqual(headers["X-Custom-Header"], "custom_value") + self.assertIn("X-Application-Name", headers) + self.assertEqual(headers["X-Application-Name"], "TestApp") + + @patch("mediawikiapi.requestsession.requests.Session") + def test_combined_auth_and_custom_headers(self, mock_session_class: Any) -> None: + """Verify that access token and custom headers work together""" + # Setup mock session + mock_session = Mock() + mock_session_class.return_value = mock_session + mock_response = Mock() + mock_response.json.return_value = { + "query": {"search": [{"title": "Test Page", "pageid": 123}]} + } + mock_session.get.return_value = mock_response + + # Create combined config + access_token = "test_token_12345" + custom_headers = {"X-App-Version": "1.0.0"} + config = Config(access_token=access_token, custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + # Make a request with unique query + api.search("combined auth test") + + # Verify both sets of headers + mock_session.get.assert_called() + call_args = mock_session.get.call_args + headers = call_args[1]["headers"] + + # Check Authorization header + self.assertIn("Authorization", headers) + self.assertEqual(headers["Authorization"], f"Bearer {access_token}") + + # Check custom header + self.assertIn("X-App-Version", headers) + self.assertEqual(headers["X-App-Version"], "1.0.0") + + # Check default User-Agent is still present + self.assertIn("User-Agent", headers) + + @patch("mediawikiapi.requestsession.requests.Session") + def test_custom_user_agent_override(self, mock_session_class: Any) -> None: + """Verify that custom User-Agent can override default""" + # Setup mock session + mock_session = Mock() + mock_session_class.return_value = mock_session + mock_response = Mock() + mock_response.json.return_value = { + "query": {"search": [{"title": "Test Page", "pageid": 123}]} + } + mock_session.get.return_value = mock_response + + # Create config with custom User-Agent + custom_user_agent = "MyCustomBot/1.0" + custom_headers = {"User-Agent": custom_user_agent} + config = Config(custom_headers=custom_headers) + api = MediaWikiAPI(config=config) + + # Make a request with unique query to avoid cache + api.search("user agent override test") + + # Verify custom User-Agent was used + mock_session.get.assert_called() + call_args = mock_session.get.call_args + headers = call_args[1]["headers"] + + self.assertEqual(headers["User-Agent"], custom_user_agent) + self.assertNotEqual(headers["User-Agent"], Config.DEFAULT_USER_AGENT) + + @patch("mediawikiapi.requestsession.requests.Session") + def test_no_auth_token_by_default(self, mock_session_class: Any) -> None: + """Verify that Authorization header is not present without access_token""" + # Setup mock session + mock_session = Mock() + mock_session_class.return_value = mock_session + mock_response = Mock() + mock_response.json.return_value = { + "query": {"search": [{"title": "Test Page", "pageid": 123}]} + } + mock_session.get.return_value = mock_response + + # Create config without token + config = Config() + api = MediaWikiAPI(config=config) + + # Make a request with unique query + api.search("no auth test") + + # Verify no Authorization header + mock_session.get.assert_called() + call_args = mock_session.get.call_args + headers = call_args[1]["headers"] + + self.assertNotIn("Authorization", headers) + self.assertIn("User-Agent", headers) # But User-Agent should be present + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_config.py b/tests/test_config.py index 2bfa26a..4778238 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -79,3 +79,48 @@ def test_set_custom_language(self) -> None: self.assertEqual(config.language, fr_lang) config.language = Language(uk_lang) # type:ignore self.assertEqual(config.language, uk_lang) + + def test_default_headers(self) -> None: + """Test that default headers include User-Agent""" + config = Config() + headers = config.get_headers() + self.assertIn("User-Agent", headers) + self.assertEqual(headers["User-Agent"], Config.DEFAULT_USER_AGENT) + + def test_access_token_in_headers(self) -> None: + """Test that access_token is included in Authorization header""" + access_token = "test_token_12345" + config = Config(access_token=access_token) + headers = config.get_headers() + self.assertIn("Authorization", headers) + self.assertEqual(headers["Authorization"], f"Bearer {access_token}") + + def test_custom_headers(self) -> None: + """Test that custom headers are included""" + custom_headers = {"X-Custom-Header": "custom_value", "X-Another": "another"} + config = Config(custom_headers=custom_headers) + headers = config.get_headers() + self.assertIn("X-Custom-Header", headers) + self.assertEqual(headers["X-Custom-Header"], "custom_value") + self.assertIn("X-Another", headers) + self.assertEqual(headers["X-Another"], "another") + + def test_custom_headers_override_defaults(self) -> None: + """Test that custom headers can override default User-Agent""" + custom_user_agent = "MyCustomBot/1.0" + custom_headers = {"User-Agent": custom_user_agent} + config = Config(custom_headers=custom_headers) + headers = config.get_headers() + self.assertEqual(headers["User-Agent"], custom_user_agent) + + def test_access_token_and_custom_headers(self) -> None: + """Test that access_token and custom headers work together""" + access_token = "test_token_12345" + custom_headers = {"X-Custom": "value"} + config = Config(access_token=access_token, custom_headers=custom_headers) + headers = config.get_headers() + self.assertIn("Authorization", headers) + self.assertEqual(headers["Authorization"], f"Bearer {access_token}") + self.assertIn("X-Custom", headers) + self.assertEqual(headers["X-Custom"], "value") + self.assertIn("User-Agent", headers)