diff --git a/test/test_wbi_helpers.py b/test/test_wbi_helpers.py index fbad77e0..1d16152f 100644 --- a/test/test_wbi_helpers.py +++ b/test/test_wbi_helpers.py @@ -3,6 +3,7 @@ mapping), search, merge, SPARQL and the various pure helper functions. """ import logging +from types import MappingProxyType import pytest import requests @@ -144,6 +145,46 @@ def test_assert_user_and_bot_flags(self, wikibase, item_q582): assert wikibase.last_request['assert'] == 'bot' +class TestRequestDataNotMutated: + """The caller's request payload must be left untouched by the API helpers.""" + + def test_helper_does_not_mutate_caller_data(self, wikibase, item_q582): + login = FakeLogin(mediawiki_api_url=wikibase.mediawiki_api_url) + data = {'action': 'wbgetentities', 'ids': 'Q582'} + + mediawiki_api_call_helper(data=data, login=login, is_bot=True) + + assert data == {'action': 'wbgetentities', 'ids': 'Q582'} + # The copy sent over the wire still gets the automatic parameters + assert wikibase.last_request['token'] == login.edit_token + assert wikibase.last_request['assert'] == 'bot' + assert wikibase.last_request['maxlag'] == '5' + assert wikibase.last_request['format'] == 'json' + + def test_api_call_does_not_mutate_caller_data(self, requests_mock): + url = 'https://example.org/w/api.php' + requests_mock.post(url, json={'success': 1}) + data = {'action': 'query'} + + mediawiki_api_call('POST', mediawiki_api_url=url, data=data) + + assert data == {'action': 'query'} + assert 'format=json' in requests_mock.last_request.text + + def test_api_call_accepts_read_only_mapping(self, requests_mock): + url = 'https://example.org/w/api.php' + requests_mock.post(url, json={'success': 1}) + + assert mediawiki_api_call('POST', mediawiki_api_url=url, data=MappingProxyType({'action': 'query'})) == {'success': 1} + assert 'format=json' in requests_mock.last_request.text + + def test_helper_accepts_none_data(self, requests_mock): + url = 'https://example.org/w/api.php' + requests_mock.post(url, json={'success': 1}) + + assert mediawiki_api_call_helper(data=None, mediawiki_api_url=url, allow_anonymous=True) == {'success': 1} + + class TestUserAgent: def test_no_warning_when_user_agent_set(self, wikibase, item_q582, caplog): data = {'action': 'wbgetentities', 'ids': 'Q582', 'format': 'json'} diff --git a/wikibaseintegrator/wbi_helpers.py b/wikibaseintegrator/wbi_helpers.py index 18e86f7e..e48b239d 100644 --- a/wikibaseintegrator/wbi_helpers.py +++ b/wikibaseintegrator/wbi_helpers.py @@ -66,6 +66,7 @@ def mediawiki_api_call(method: str, mediawiki_api_url: str | None = None, sessio # TODO: Add support for 'multipart/form-data' when using POST (https://www.mediawiki.org/wiki/API:Edit#Large_edits) if 'data' in kwargs and kwargs['data']: + kwargs['data'] = dict(kwargs['data']) if 'format' not in kwargs['data']: kwargs['data'].update({'format': 'json'}) elif kwargs['data']['format'] != 'json': @@ -165,6 +166,7 @@ def mediawiki_api_call_helper(data: dict[str, Any], login: _Login | None = None, """ mediawiki_api_url = str(mediawiki_api_url or config['MEDIAWIKI_API_URL']) user_agent = user_agent or (str(config['USER_AGENT']) if config['USER_AGENT'] is not None else None) + data = dict(data) if data is not None else None hostname = urlparse(mediawiki_api_url).hostname if hostname is not None and hostname.endswith(('wikidata.org', 'wikipedia.org', 'wikimedia.org')) and user_agent is None: