-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
tests: Add minimal tests for
githost.GitHubApi
- Loading branch information
Showing
4 changed files
with
152 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
pytest_plugins = ("tests.requests_mocker",) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,107 @@ | ||
"""A mocker for ``urllib.request.urlopen``.""" | ||
|
||
import collections | ||
import dataclasses | ||
import json as jsonlib | ||
import typing | ||
import unittest.mock | ||
import urllib.parse | ||
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.encode("utf-8"), 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=dict(request.headers), # MutableMapping -> dict | ||
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] # type: ignore [assignment] | ||
return params |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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" |