Skip to content

Commit

Permalink
Merge pull request #7 from zyv/feature/migrate-respx
Browse files Browse the repository at this point in the history
Migrate to `respx` from `pytest_httpx` and add override tests
  • Loading branch information
zyv authored Feb 21, 2025
2 parents b782689 + d423c8c commit 6d0e2c1
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 102 deletions.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies = [
dev = [
"hatch>=1.14.0",
"pytest-cov>=6.0.0",
"pytest-httpx>=0.35.0",
"respx>=0.22.0",
"ruff>=0.9.5",
]

Expand Down
119 changes: 56 additions & 63 deletions tests/test_client.py
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
import json
import re
from datetime import UTC, datetime
from http import HTTPMethod
from unittest.mock import MagicMock

import httpx
import pytest
from httpx._config import DEFAULT_TIMEOUT_CONFIG
import respx
from pydantic import HttpUrl
from pytest_httpx import HTTPXMock

from github_issues_import.client import ApiClient, HttpClient
from github_issues_import.models import IssueImportRequest, IssueImportStatus, IssueImportStatusResponse

from .utils import get_fixture

RESPONSE_STATUS_MULTIPLE_ISSUES = httpx.Response(
httpx.codes.OK,
text=get_fixture("response-multiple-check-status-of-multiple-issues.json"),
)

GITHUB_TOKEN = "ghp_abc123"


Expand All @@ -22,74 +26,71 @@ def api_client():
return ApiClient(http_client=HttpClient(token=GITHUB_TOKEN))


def test_init(api_client: ApiClient):
def test_simple_client_init(api_client: ApiClient):
assert api_client


def test_timeout_default(httpx_mock: HTTPXMock):
httpx_mock.add_response(
match_extensions={
"timeout": {
"connect": DEFAULT_TIMEOUT_CONFIG.connect,
"read": DEFAULT_TIMEOUT_CONFIG.read,
"write": DEFAULT_TIMEOUT_CONFIG.write,
"pool": DEFAULT_TIMEOUT_CONFIG.pool,
}
},
status_code=httpx.codes.BAD_GATEWAY,
)
with pytest.raises(httpx.HTTPStatusError):
ApiClient(http_client=HttpClient(token=GITHUB_TOKEN)).get_status_multiple("foo", "bar", datetime.now(tz=UTC))
def test_base_url_override(respx_mock: respx.mock):
respx_mock.get(url="https://test/repos/foo/bar/import/issues").mock(return_value=RESPONSE_STATUS_MULTIPLE_ISSUES)

api_client = ApiClient(http_client=HttpClient(token=GITHUB_TOKEN, base_url="https://test"))
api_client.get_status_multiple("foo", "bar", datetime.now(tz=UTC))

def test_timeout_set(httpx_mock: HTTPXMock):
httpx_mock.add_response(
match_extensions={"timeout": {"connect": 123, "read": 123, "write": 123, "pool": 123}},
status_code=httpx.codes.BAD_GATEWAY,
)
with pytest.raises(httpx.HTTPStatusError):
ApiClient(http_client=HttpClient(token=GITHUB_TOKEN, timeout=123)).get_status_multiple(
"foo", "bar", datetime.now(tz=UTC)
)

def test_headers_override(respx_mock: respx.mock):
respx_mock.get(headers={"Foo": "Bar"}).mock(return_value=RESPONSE_STATUS_MULTIPLE_ISSUES)

api_client = ApiClient(http_client=HttpClient(token=GITHUB_TOKEN, headers={"Foo": "Bar"}))
api_client.get_status_multiple("foo", "bar", datetime.now(tz=UTC))

assert "Authorization" not in respx_mock.calls.last.request.headers

def test_base_url(httpx_mock: HTTPXMock):
httpx_mock.add_response(
url=re.compile(r"^https://test/repos/foo/bar/import/issues"),
text=get_fixture("response-multiple-check-status-of-multiple-issues.json"),
)
ApiClient(http_client=HttpClient(token=GITHUB_TOKEN, base_url="https://test")).get_status_multiple(
"foo", "bar", datetime.now(tz=UTC)
)

def test_event_hooks_override(monkeypatch, respx_mock: respx.mock):
respx_mock.get().mock(return_value=RESPONSE_STATUS_MULTIPLE_ISSUES)

def test_event_hooks(monkeypatch, httpx_mock: HTTPXMock):
httpx_mock.add_response(text=get_fixture("response-multiple-check-status-of-multiple-issues.json"))
mock_log_request, mock_log_response = MagicMock(), MagicMock()
monkeypatch.setattr(HttpClient, "log_github_api_request", mock_log_request)
monkeypatch.setattr(HttpClient, "log_github_api_response", mock_log_response)

mock = MagicMock()
monkeypatch.setattr(HttpClient, "log_github_api_request", mock)
api_client = ApiClient(http_client=HttpClient(token=GITHUB_TOKEN, base_url="https://test", event_hooks={}))
api_client.get_status_multiple("foo", "bar", datetime.now(tz=UTC))

ApiClient(http_client=HttpClient(token=GITHUB_TOKEN, base_url="https://test", event_hooks={})).get_status_multiple(
"foo", "bar", datetime.now(tz=UTC)
mock_log_request.assert_not_called()
mock_log_response.assert_not_called()


def test_default_event_hooks(monkeypatch, respx_mock: respx.mock):
respx_mock.post("https://api.github.com/repos/foo/bar/import/issues").mock(
return_value=httpx.Response(httpx.codes.BAD_GATEWAY)
)
mock.assert_not_called()

mock_log_request, mock_log_response = MagicMock(), MagicMock()
monkeypatch.setattr(HttpClient, "log_github_api_request", mock_log_request)
monkeypatch.setattr(HttpClient, "log_github_api_response", mock_log_response)

api_client = ApiClient(http_client=HttpClient(token=GITHUB_TOKEN))

def test_raise_for_status(api_client: ApiClient, httpx_mock: HTTPXMock):
httpx_mock.add_response(status_code=httpx.codes.BAD_GATEWAY)
with pytest.raises(httpx.HTTPError):
with pytest.raises(httpx.HTTPStatusError):
api_client.import_issue(
"foo",
"bar",
IssueImportRequest.model_validate_json(get_fixture("request-issue-and-comment-fields.json")),
)

mock_log_request.assert_called()
mock_log_response.assert_called()


def test_import_issue(api_client: ApiClient, httpx_mock: HTTPXMock):
def test_import_issue(api_client: ApiClient, respx_mock: respx.mock):
import_response = get_fixture("response-single-start-an-issue-import.json")
import_request = get_fixture("request-start-issue-import.json")

httpx_mock.add_response(text=import_response)
respx_mock.route(
method=HTTPMethod.POST,
url="https://api.github.com/repos/owner/repository/import/issues",
headers={"Authorization": f"Token {GITHUB_TOKEN}"} | HttpClient.HEADERS,
).mock(return_value=httpx.Response(httpx.codes.ACCEPTED, text=import_response))

response = api_client.import_issue(
"owner",
Expand All @@ -100,36 +101,28 @@ def test_import_issue(api_client: ApiClient, httpx_mock: HTTPXMock):
assert response == IssueImportStatusResponse.model_validate_json(import_response)
assert response.status == IssueImportStatus.PENDING

request = httpx_mock.get_request()

assert request.url == "https://api.github.com/repos/owner/repository/import/issues"
assert request.headers["Authorization"] == f"Token {GITHUB_TOKEN}"
assert json.loads(request.content) == json.loads(import_request)


def test_get_import_status(api_client: ApiClient, httpx_mock: HTTPXMock):
def test_get_import_status(api_client: ApiClient, respx_mock: respx.mock):
import_status_response = get_fixture("response-single-check-status-of-issue-import.json")

httpx_mock.add_response(
url="https://api.github.com/repos/jonmagic/foo/import/issues/3",
text=import_status_response,
respx_mock.get("https://api.github.com/repos/jonmagic/foo/import/issues/3").mock(
return_value=httpx.Response(httpx.codes.OK, text=import_status_response)
)

response = api_client.get_status(HttpUrl("https://api.github.com/repos/jonmagic/foo/import/issues/3"))
assert response == IssueImportStatusResponse.model_validate_json(import_status_response)


def test_get_import_status_multiple(api_client: ApiClient, httpx_mock: HTTPXMock):
def test_get_import_status_multiple(api_client: ApiClient, respx_mock: respx.mock):
multiple_status_response = get_fixture("response-multiple-check-status-of-multiple-issues.json")
since = datetime.now(tz=UTC)

httpx_mock.add_response(
url=httpx.URL(
respx_mock.get(
httpx.URL(
"https://api.github.com/repos/foo/bar/import/issues",
params={"since": since.isoformat()},
),
text=multiple_status_response,
)
)
).mock(return_value=RESPONSE_STATUS_MULTIPLE_ISSUES)

response = api_client.get_status_multiple("foo", "bar", since)
assert response == [IssueImportStatusResponse.model_validate(json.loads(multiple_status_response)[0])]
Loading

0 comments on commit 6d0e2c1

Please sign in to comment.