-
Notifications
You must be signed in to change notification settings - Fork 61
Open
Labels
Description
I have the below code to test a praw reddit bot. The first test always succeeds, but follow up tests fail to find the required calls in the cassette. I discovered that it is related to the filter_access_token
callback, and if I disable that, then follow up tests will succeed. Am I doing something wrong in the callback function?
from base64 import b64encode
import json
import os
from urllib.parse import quote_plus
from betamax import Betamax, cassette
from betamax_serializers.pretty_json import PrettyJSONSerializer
from praw.config import _NotSet
import pytest
from praw.models import Submission, Comment
from src.reddit.bot import Bot
Betamax.register_serializer(PrettyJSONSerializer)
def b64_string(input_string: str) -> str:
"""Return a base64 encoded string (not bytes) from input_string."""
return b64encode(input_string.encode('utf-8')).decode('utf-8')
def filter_access_token(
interaction: cassette.cassette.Interaction,
current_cassette: cassette.cassette.Cassette,
):
"""Add Betamax placeholder to filter access token."""
request_uri = interaction.data['request']['uri']
response = interaction.data['response']
# We only care about requests that generate an access token.
if ('api/v1/access_token' not in request_uri or
response['status']['code'] != 200):
return
body = response['body']['string']
token_types = [
'access_token',
'refresh_token',
]
for token_type in token_types:
try:
token = json.loads(body)[token_type]
except (KeyError, TypeError, ValueError):
continue
# If we succeeded in finding the token, add it to the placeholders for this cassette.
current_cassette.placeholders.append(
cassette.cassette.Placeholder(placeholder=f'<{token_type.upper()}>', replace=token)
)
def get_placeholders(bot: Bot) -> dict[str, str]:
"""Prepare placeholders for sensitive information."""
filter_keys = [
'client_id',
'client_secret',
'password',
'username',
]
placeholders = {
attr: getattr(bot.reddit.config, attr)
for attr in filter_keys}
# Check if attributes exist and are not _NotSet before using them
for key in placeholders:
if isinstance(placeholders[key], _NotSet):
placeholders[key] = ''
# Password is sent URL-encoded.
placeholders['password'] = quote_plus(placeholders['password'])
# Client ID and secret are sent in base-64 encoding.
placeholders['basic_auth'] = b64_string(
"{}:{}".format(placeholders['client_id'],
placeholders['client_secret'])
)
return placeholders
class TestBot:
@pytest.fixture(scope='session')
def bot(self):
return Bot(
user_agent='Test suite',
)
@pytest.fixture(scope='session', autouse=True)
def betamax_config(self, bot):
with Betamax.configure() as config:
config.cassette_library_dir = 'tests/fixtures/cassettes'
config.default_cassette_options['serialize_with'] = 'prettyjson'
config.before_record(callback=filter_access_token)
# Add placeholders for sensitive information.
for key, value in get_placeholders(bot).items():
config.define_cassette_placeholder('<{}>'.format(key.upper()), value)
@pytest.fixture(scope='session')
def session(self, bot):
http = bot.reddit._core._requestor._http
http.headers['Accept-Encoding'] = 'identity' # ensure response is human readable
return http
@pytest.fixture(scope='session')
def recorder(self, session):
return Betamax(session)
def test_submission(self, bot, recorder, request):
submission = bot.reddit.submission(id='w03cku')
with recorder.use_cassette(request.node.name):
assert submission.author
I get the following error.
self = <prawcore.requestor.Requestor object at 0x00000170BD94D8D0>, timeout = None, args = ('post', 'https://www.reddit.com/api/v1/access_token')
kwargs = {'auth': ('**********************', '******************************'), 'data': [('grant_type', 'password'), ('password', '****************'), ('username', '**************')], 'headers': {'Connection': 'close'}}
def request(
self, *args: Any, timeout: float | None = None, **kwargs: Any
) -> Response:
"""Issue the HTTP request capturing any errors that may occur."""
try:
return self._http.request(*args, timeout=timeout or self.timeout, **kwargs)
except Exception as exc: # noqa: BLE001
> raise RequestException(exc, args, kwargs) from None
E prawcore.exceptions.RequestException: error with request A request was made that could not be handled.
E
E A request was made to https://www.reddit.com/api/v1/access_token that could not be found in test_submission.
E
E The settings on the cassette are:
E
E - record_mode: once
E - match_options {'uri', 'method'}.
venv\Lib\site-packages\prawcore\requestor.py:70: RequestException
--------------------------------------------------------------------------------------------------------- Captured log call ---------------------------------------------------------------------------------------------------------
WARNING prawcore:sessions.py:161 Retrying due to ChunkedEncodingError(ProtocolError('Connection broken: IncompleteRead(93 bytes read, 755 more expected)', IncompleteRead(93 bytes read, 755 more expected))) status: GET https://oauth.reddit.com/comments/w03cku/
I've been trying to follow this (somewhat outdated) blog/tutorial (https://leviroth.github.io/2017-05-16-testing-reddit-bots-with-betamax/) on using betamax and I believe this is the last issue I have to solve.
Edit:
I did find that I can use
cassette.cassette.Placeholder(placeholder='*' * len(token), replace=token)
instead of
cassette.cassette.Placeholder(placeholder=f'<{token_type.upper()}>', replace=token)
and it will work... but I'm not sure that's the best solution.