diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..596f1d3 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1 @@ +pytest_plugins = ("tests.requests_mocker",) diff --git a/tests/requests_mocker.py b/tests/requests_mocker.py new file mode 100644 index 0000000..e045309 --- /dev/null +++ b/tests/requests_mocker.py @@ -0,0 +1,106 @@ +"""A mocker for ``urllib.request.urlopen``.""" + +import collections +import dataclasses +import json as jsonlib +import typing +import unittest.mock +import urllib.request + +import pytest + + +@pytest.fixture(name="requests_mocker") +def get_mock(): + """Return an instance of ``Mock`` to be used as a fixture. + + Example:: + + with get_mock() as mock: + mock.register("GET", "https://example.com", content="OK") + # call code that would make an HTTP request + """ + m = Mock() + with unittest.mock.patch("urllib.request.urlopen", m.urlopen): + yield m + + +@dataclasses.dataclass +class Request: + url: str + headers: typing.Dict[str, str] + data: bytes + params: dict + + +@dataclasses.dataclass +class Response: + content: bytes + status: int + + def read(self): + return self.content + + +@dataclasses.dataclass +class Call: + request: Request + response: Response + + +class Mock(): + """Intercept HTTP requests and mock their responses. + + An instance of ``Mock`` can be configured via its two methods: + + - ``get(url: str, json: object, status=200)`` allows you to mock + the response of a ``GET`` request to particular URL. + + - ``register(method: str, url: str, content: bytes, status=200)`` + is a more generic method. + """ + def __init__(self): + self.mocks = collections.defaultdict(dict) + self.calls = [] + + def register(self, method: str, url: str, content: bytes, status: int = 200): + method = method.lower() + self.mocks[url][method] = Response(content=content, status=status) + + def get(self, url: str, json: object, status: int = 200): + content = jsonlib.dumps(json) + self.register("get", url, content=content, status=status) + + def urlopen(self, request: urllib.request.Request, **kwargs): + method = request.get_method().lower() + url = _strip_query_string(request.full_url) + response = self.mocks.get(url, {}).get(method) + if not response: + raise ValueError(f"No mock for method={method} and url={url}") + call = Call( + request=Request( + url=url, + headers=request.headers, # type: ignore[arg-type] + data=request.data or b'', # type: ignore[arg-type] + params=_extract_params(request), + ), + response=response, + ) + self.calls.append(call) + return response + + +def _strip_query_string(url: str) -> str: + parsed = urllib.parse.urlparse(url) + return parsed._replace(query="").geturl() + + +def _extract_params(request: urllib.request.Request) -> dict: + query = urllib.parse.urlparse(request.full_url).query + if not query: + return {} + params = urllib.parse.parse_qs(query) + for key, values in params.items(): + if len(values) == 1: + params[key] = values[0] + return params diff --git a/tests/test_githost.py b/tests/test_githost.py new file mode 100644 index 0000000..0f0b817 --- /dev/null +++ b/tests/test_githost.py @@ -0,0 +1,28 @@ +import os +from unittest import mock + +from check_oldies import branches +from check_oldies import githost + + +FAKE_GITHUB_API_RESPONSE = [ + { + "number": 1234, + "state": "open", + "html_url": "https://github.com/polyconseil/check-oldies/pull/1234", + }, +] + + +@mock.patch.dict(os.environ, {"TOKEN": "secret"}, clear=True) +def test_github_api(requests_mocker): + requests_mocker.get( + "https://api.github.com/repos/polyconseil/check-oldies/pulls", + json=FAKE_GITHUB_API_RESPONSE, + ) + api_access = branches.GitHostApiAccessInfo(auth_token_env_var="TOKEN") + api = githost.GitHubApi("polyconseil", api_access) + pull_request = api.get_pull_request("check-oldies", "my-branch") + assert pull_request.number == 1234 + assert pull_request.state == "open" + assert pull_request.url == "https://github.com/polyconseil/check-oldies/pull/1234" diff --git a/tests/test_requests_mocker.py b/tests/test_requests_mocker.py new file mode 100644 index 0000000..74197b3 --- /dev/null +++ b/tests/test_requests_mocker.py @@ -0,0 +1,16 @@ +import urllib.request + +from . import requests_mocker + + +def test_extract_params(): + url = "https://example.com/?single=1&multiple=2&multiple=3&empty=" + request = urllib.request.Request(url) + params = requests_mocker._extract_params(request) + assert params == {"single": "1", "multiple": ["2", "3"]} + + +def test_strip_query_string(): + url = "https://example.com/path?foo=1" + stripped = requests_mocker._strip_query_string(url) + assert stripped == "https://example.com/path"