Skip to content

Commit

Permalink
✔️ 100% coverage for the asynchronous part
Browse files Browse the repository at this point in the history
  • Loading branch information
Ousret committed Oct 26, 2024
1 parent 2b545a5 commit a268836
Show file tree
Hide file tree
Showing 12 changed files with 1,265 additions and 1 deletion.
3 changes: 2 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@ lint = [
]
test = [
"betamax >=0.8, <0.9",
"pytest ==7.*"
"pytest ==7.*",
"pytest-asyncio>=0.20,<0.25"
]

[project.urls]
Expand Down
38 changes: 38 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import os
import socket
import time
import asyncio
from base64 import b64encode
from sys import platform, modules

Expand Down Expand Up @@ -35,6 +36,11 @@
import pytest

from prawcore import Requestor, TrustedAuthenticator, UntrustedAuthenticator
from prawcore import (
AsyncRequestor,
AsyncTrustedAuthenticator,
AsyncUntrustedAuthenticator,
)


@pytest.fixture(autouse=True)
Expand All @@ -48,6 +54,17 @@ def _sleep(*_, **__):
monkeypatch.setattr(time, "sleep", value=_sleep)


@pytest.fixture(autouse=True)
def patch_async_sleep(monkeypatch):
"""Auto patch sleep to speed up tests."""

async def _sleep(*_, **__):
"""Dud sleep function."""
pass

monkeypatch.setattr(asyncio, "sleep", value=_sleep)


@pytest.fixture
def image_path():
"""Return path to image."""
Expand All @@ -65,6 +82,11 @@ def requestor():
return Requestor("prawcore:test (by /u/bboe)")


@pytest.fixture
def async_requestor():
return AsyncRequestor("prawcore:test (by /u/bboe)")


@pytest.fixture
def trusted_authenticator(requestor):
"""Return a TrustedAuthenticator instance."""
Expand All @@ -75,12 +97,28 @@ def trusted_authenticator(requestor):
)


@pytest.fixture
def async_trusted_authenticator(async_requestor):
"""Return a TrustedAuthenticator instance."""
return AsyncTrustedAuthenticator(
async_requestor,
pytest.placeholders.client_id,
pytest.placeholders.client_secret,
)


@pytest.fixture
def untrusted_authenticator(requestor):
"""Return an UntrustedAuthenticator instance."""
return UntrustedAuthenticator(requestor, pytest.placeholders.client_id)


@pytest.fixture
def async_untrusted_authenticator(async_requestor):
"""Return an UntrustedAuthenticator instance."""
return AsyncUntrustedAuthenticator(async_requestor, pytest.placeholders.client_id)


def env_default(key):
"""Return environment variable or placeholder string."""
return os.environ.get(
Expand Down
115 changes: 115 additions & 0 deletions tests/integration/asynchronous/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
"""prawcore Integration test suite."""
from __future__ import annotations

import base64
import io
import os

import betamax
import pytest
from betamax.cassette import Cassette, Interaction
from betamax.util import body_io

from niquests import PreparedRequest, Response

from niquests.adapters import AsyncHTTPAdapter
from niquests.utils import _swap_context

try:
from urllib3 import AsyncHTTPResponse, HTTPHeaderDict
from urllib3.backend._async import AsyncLowLevelResponse
except ImportError:
from urllib3_future import AsyncHTTPResponse, HTTPHeaderDict
from urllib3_future.backend._async import AsyncLowLevelResponse

CASSETTES_PATH = "tests/integration/cassettes"
existing_cassettes = set()
used_cassettes = set()


@pytest.mark.asyncio
class AsyncIntegrationTest:
"""Base class for prawcore integration tests."""

@pytest.fixture(autouse=True)
def inject_fake_async_response(self, cassette_name, monkeypatch):
"""betamax does not support Niquests async capabilities.
This fixture is made to compensate for this missing feature.
"""
cassette_base_dir = os.path.join(os.path.dirname(__file__), "..", "cassettes")
cassette = Cassette(cassette_name, serialization_format="json", cassette_library_dir=cassette_base_dir)
cassette.match_options.update({"method"})

def patch_add_urllib3_response(serialized, response, headers):
"""This function is patched so that we can construct a proper async dummy response."""
if 'base64_string' in serialized['body']:
body = io.BytesIO(
base64.b64decode(serialized['body']['base64_string'].encode())
)
else:
body = body_io(**serialized['body'])

async def fake_inner_read(*args) -> tuple[bytes, bool, HTTPHeaderDict | None]:
"""Fake the async iter socket read from AsyncHTTPConnection down in urllib3-future."""
nonlocal body
return body.getvalue(), True, None

fake_llr = AsyncLowLevelResponse(
method="GET", # hardcoded, but we don't really care. It does not impact the tests.
status=response.status_code,
reason=response.reason,
headers=headers,
body=fake_inner_read,
version=11,
)

h = AsyncHTTPResponse(
body,
status=response.status_code,
reason=response.reason,
headers=headers,
original_response=fake_llr,
)

response.raw = h

monkeypatch.setattr(betamax.util, "add_urllib3_response", patch_add_urllib3_response)

async def fake_send(_, *args, **kwargs) -> Response:
nonlocal cassette

prep_request: PreparedRequest = args[0]

interaction: Interaction | None = cassette.find_match(prep_request)

if interaction:
# betamax can generate a requests.Response
# from a matched interaction.
# three caveats:
# first: not async compatible
# second: we need to output niquests.AsyncResponse first
# third: the underlying HTTPResponse is sync bound

resp = interaction.as_response()
# Niquests have two kind of responses in async mode.
# A) Response (in case stream=False)
# B) AsyncResponse (in case stream=True)
_swap_context(resp)

return resp

raise Exception("no match in cassettes for this request.")

AsyncHTTPAdapter.send = fake_send

@pytest.fixture
def cassette_name(self, request):
"""Return the name of the cassette to use."""
marker = request.node.get_closest_marker("cassette_name")
if marker is None:
return (
f"{request.cls.__name__}.{request.node.name}"
if request.cls
else request.node.name
)
return marker.args[0]
41 changes: 41 additions & 0 deletions tests/integration/asynchronous/test_authenticator.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""Test for subclasses of prawcore.auth.BaseAuthenticator class."""

import pytest

import prawcore

from . import AsyncIntegrationTest


class TestTrustedAuthenticator(AsyncIntegrationTest):
async def test_revoke_token(self, async_requestor):
authenticator = prawcore.AsyncTrustedAuthenticator(
async_requestor,
pytest.placeholders.client_id,
pytest.placeholders.client_secret,
)
await authenticator.revoke_token("dummy token")

async def test_revoke_token__with_access_token_hint(self, async_requestor):
authenticator = prawcore.AsyncTrustedAuthenticator(
async_requestor,
pytest.placeholders.client_id,
pytest.placeholders.client_secret,
)
await authenticator.revoke_token("dummy token", "access_token")

async def test_revoke_token__with_refresh_token_hint(self, async_requestor):
authenticator = prawcore.AsyncTrustedAuthenticator(
async_requestor,
pytest.placeholders.client_id,
pytest.placeholders.client_secret,
)
await authenticator.revoke_token("dummy token", "refresh_token")


class TestUntrustedAuthenticator(AsyncIntegrationTest):
async def test_revoke_token(self, async_requestor):
authenticator = prawcore.AsyncUntrustedAuthenticator(
async_requestor, pytest.placeholders.client_id
)
await authenticator.revoke_token("dummy token")
Loading

0 comments on commit a268836

Please sign in to comment.