Skip to content

Commit

Permalink
Add pre/post refresh callbacks
Browse files Browse the repository at this point in the history
  • Loading branch information
bboe committed Feb 22, 2021
1 parent bb0e00a commit bb0e002
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 34 deletions.
17 changes: 17 additions & 0 deletions CHANGES.rst
Expand Up @@ -4,6 +4,23 @@ Change Log
prawcore follows `semantic versioning <http://semver.org/>`_ with the exception that
deprecations will not be announced by a minor release.

Unreleased
----------

**Added**

* ``Authorizer`` optionally takes a ``pre_refresh_callback`` keyword
argument. If provided, the function will called with the instance of
``Authorizer`` prior to refreshing the access and refresh tokens.
* ``Authorizer`` optionally takes a ``post_refresh_callback`` keyword
argument. If provided, the function will called with the instance of
``Authorizer`` after refreshing the access and refresh tokens.

**Changed**

* The ``refresh_token`` argument to ``Authorizer`` must now be passed by
keyword, and cannot be passed as a positional argument.

1.5.0 (2020-08-04)
------------------

Expand Down
25 changes: 24 additions & 1 deletion prawcore/auth.py
Expand Up @@ -200,15 +200,34 @@ class Authorizer(BaseAuthorizer):

AUTHENTICATOR_CLASS = BaseAuthenticator

def __init__(self, authenticator, refresh_token=None):
def __init__(
self,
authenticator,
*,
post_refresh_callback=None,
pre_refresh_callback=None,
refresh_token=None,
):
"""Represent a single authorization to Reddit's API.
:param authenticator: An instance of a subclass of :class:`BaseAuthenticator`.
:param post_refresh_callback: (Optional) When a single-argument
function is passed, the function will be called prior to refreshing
the access and refresh tokens. The argument to the callback is the
:class:`Authorizer` instance. This callback can be used to inspect
and modify the attributes of the :class:`Authorizer`.
:param pre_refresh_callback: (Optional) When a single-argument function
is passed, the function will be called after refreshing the access
and refresh tokens. The argument to the callback is the
:class:`Authorizer` instance. This callback can be used to inspect
and modify the attributes of the :class:`Authorizer`.
:param refresh_token: (Optional) Enables the ability to refresh the
authorization.
"""
super(Authorizer, self).__init__(authenticator)
self._post_refresh_callback = post_refresh_callback
self._pre_refresh_callback = pre_refresh_callback
self.refresh_token = refresh_token

def authorize(self, code):
Expand All @@ -228,11 +247,15 @@ def authorize(self, code):

def refresh(self):
"""Obtain a new access token from the refresh_token."""
if self._pre_refresh_callback:
self._pre_refresh_callback(self)
if self.refresh_token is None:
raise InvalidInvocation("refresh token not provided")
self._request_token(
grant_type="refresh_token", refresh_token=self.refresh_token
)
if self._post_refresh_callback:
self._post_refresh_callback(self)

def revoke(self, only_access=False):
"""Revoke the current Authorization.
Expand Down
91 changes: 66 additions & 25 deletions tests/cassettes/Authorizer_refresh.json
@@ -1,45 +1,86 @@
{
"http_interactions": [
{
"recorded_at": "2016-02-07T00:06:41",
"recorded_at": "2021-02-22T05:34:53",
"request": {
"body": {
"encoding": "utf-8",
"string": "grant_type=refresh_token&refresh_token=<REFRESH_TOKEN>"
},
"headers": {
"Accept": "*/*",
"Accept-Encoding": "gzip, deflate",
"Authorization": "Basic <BASIC_AUTH>",
"Connection": "keep-alive",
"Content-Length": "66",
"Content-Type": "application/x-www-form-urlencoded",
"User-Agent": "prawcore/0.0.1a1"
"Accept": [
"*/*"
],
"Accept-Encoding": [
"gzip, deflate"
],
"Authorization": [
"Basic <BASIC_AUTH>"
],
"Connection": [
"close"
],
"Content-Length": [
"77"
],
"Content-Type": [
"application/x-www-form-urlencoded"
],
"User-Agent": [
"prawcore:test (by /u/bboe) prawcore/1.5.0"
]
},
"method": "POST",
"uri": "https://www.reddit.com/api/v1/access_token"
},
"response": {
"body": {
"base64_string": "H4sIAAAAAAAAA6tWSkxOTi0uji/Jz07NU7JSUDI3NjCyMDPXzQk3cAlILzTPL83NdtX1TzfNCA8rSPIzTzU0UdJRUAKrjy+pLEgFaUpKTSxKLQKJp1YUZBalFsdnggwzNjMw0FFQKk7OhyjLTEnNK8ksqVSqBQDBWguReAAAAA==",
"encoding": "UTF-8",
"string": ""
"string": "{\"access_token\": \"0000000-aaaaaaaaaaaaaaaaaaaaaa-0000000\", \"token_type\": \"bearer\", \"expires_in\": 3600, \"refresh_token\": \"aaaaaaa-0000000000000000000000-aaaaaaa\", \"scope\": \"modmail\"}"
},
"headers": {
"CF-RAY": "270ad9ad977039a6-PHX",
"Connection": "keep-alive",
"Content-Encoding": "gzip",
"Content-Type": "application/json; charset=UTF-8",
"Date": "Sun, 07 Feb 2016 00:06:41 GMT",
"Server": "cloudflare-nginx",
"Set-Cookie": "__cfduid=d533ca9cde1a0f39d6b51e0a77caee21f1454803601; expires=Mon, 06-Feb-17 00:06:41 GMT; path=/; domain=.reddit.com; HttpOnly",
"Strict-Transport-Security": "max-age=15552000; includeSubDomains; preload",
"Transfer-Encoding": "chunked",
"X-Moose": "majestic",
"cache-control": "max-age=0, must-revalidate",
"x-content-type-options": "nosniff",
"x-frame-options": "SAMEORIGIN",
"x-xss-protection": "1; mode=block"
"Accept-Ranges": [
"bytes"
],
"Connection": [
"close"
],
"Content-Length": [
"181"
],
"Content-Type": [
"application/json; charset=UTF-8"
],
"Date": [
"Mon, 22 Feb 2021 05:34:53 GMT"
],
"Server": [
"snooserv"
],
"Set-Cookie": [
"edgebucket=f5KZ6I9GmO6zC9InB3; Domain=reddit.com; Max-Age=63071999; Path=/; secure"
],
"Strict-Transport-Security": [
"max-age=15552000; includeSubDomains; preload"
],
"Via": [
"1.1 varnish"
],
"X-Moose": [
"majestic"
],
"cache-control": [
"max-age=0, must-revalidate"
],
"x-content-type-options": [
"nosniff"
],
"x-frame-options": [
"SAMEORIGIN"
],
"x-xss-protection": [
"1; mode=block"
]
},
"status": {
"code": 200,
Expand All @@ -49,5 +90,5 @@
}
}
],
"recorded_with": "betamax/0.5.1"
"recorded_with": "betamax/0.8.1"
}
63 changes: 56 additions & 7 deletions tests/test_authorizer.py
Expand Up @@ -80,7 +80,9 @@ def test_initialize(self):
self.assertFalse(authorizer.is_valid())

def test_initialize__with_refresh_token(self):
authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN)
authorizer = prawcore.Authorizer(
self.authentication, refresh_token=REFRESH_TOKEN
)
self.assertIsNone(authorizer.access_token)
self.assertIsNone(authorizer.scopes)
self.assertEqual(REFRESH_TOKEN, authorizer.refresh_token)
Expand All @@ -95,7 +97,44 @@ def test_initialize__with_untrusted_authenticator(self):
self.assertFalse(authorizer.is_valid())

def test_refresh(self):
authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN)
authorizer = prawcore.Authorizer(
self.authentication, refresh_token=REFRESH_TOKEN
)
with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"):
authorizer.refresh()

self.assertIsNotNone(authorizer.access_token)
self.assertIsInstance(authorizer.scopes, set)
self.assertTrue(len(authorizer.scopes) > 0)
self.assertTrue(authorizer.is_valid())

def test_refresh__post_refresh_callback(self):
def callback(authorizer):
self.assertNotEqual(REFRESH_TOKEN, authorizer.refresh_token)
authorizer.refresh_token = "manually_updated"

authorizer = prawcore.Authorizer(
self.authentication,
post_refresh_callback=callback,
refresh_token=REFRESH_TOKEN,
)
with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"):
authorizer.refresh()

self.assertIsNotNone(authorizer.access_token)
self.assertEqual("manually_updated", authorizer.refresh_token)
self.assertIsInstance(authorizer.scopes, set)
self.assertTrue(len(authorizer.scopes) > 0)
self.assertTrue(authorizer.is_valid())

def test_refresh__pre_refresh_callback(self):
def callback(authorizer):
self.assertIsNone(authorizer.refresh_token)
authorizer.refresh_token = REFRESH_TOKEN

authorizer = prawcore.Authorizer(
self.authentication, pre_refresh_callback=callback
)
with Betamax(REQUESTOR).use_cassette("Authorizer_refresh"):
authorizer.refresh()

Expand All @@ -105,7 +144,9 @@ def test_refresh(self):
self.assertTrue(authorizer.is_valid())

def test_refresh__with_invalid_token(self):
authorizer = prawcore.Authorizer(self.authentication, "INVALID_TOKEN")
authorizer = prawcore.Authorizer(
self.authentication, refresh_token="INVALID_TOKEN"
)
with Betamax(REQUESTOR).use_cassette(
"Authorizer_refresh__with_invalid_token"
):
Expand All @@ -118,7 +159,9 @@ def test_refresh__without_refresh_token(self):
self.assertFalse(authorizer.is_valid())

def test_revoke__access_token_with_refresh_set(self):
authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN)
authorizer = prawcore.Authorizer(
self.authentication, refresh_token=REFRESH_TOKEN
)
with Betamax(REQUESTOR).use_cassette(
"Authorizer_revoke__access_token_with_refresh_set"
):
Expand Down Expand Up @@ -149,7 +192,9 @@ def test_revoke__access_token_without_refresh_set(self):
self.assertFalse(authorizer.is_valid())

def test_revoke__refresh_token_with_access_set(self):
authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN)
authorizer = prawcore.Authorizer(
self.authentication, refresh_token=REFRESH_TOKEN
)
with Betamax(REQUESTOR).use_cassette(
"Authorizer_revoke__refresh_token_with_access_set"
):
Expand All @@ -162,7 +207,9 @@ def test_revoke__refresh_token_with_access_set(self):
self.assertFalse(authorizer.is_valid())

def test_revoke__refresh_token_without_access_set(self):
authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN)
authorizer = prawcore.Authorizer(
self.authentication, refresh_token=REFRESH_TOKEN
)
with Betamax(REQUESTOR).use_cassette(
"Authorizer_revoke__refresh_token_without_access_set"
):
Expand All @@ -174,7 +221,9 @@ def test_revoke__refresh_token_without_access_set(self):
self.assertFalse(authorizer.is_valid())

def test_revoke__without_access_token(self):
authorizer = prawcore.Authorizer(self.authentication, REFRESH_TOKEN)
authorizer = prawcore.Authorizer(
self.authentication, refresh_token=REFRESH_TOKEN
)
self.assertRaises(
prawcore.InvalidInvocation, authorizer.revoke, only_access=True
)
Expand Down
4 changes: 3 additions & 1 deletion tests/test_sessions.py
Expand Up @@ -39,7 +39,9 @@ def client_authorizer():
authenticator = prawcore.TrustedAuthenticator(
REQUESTOR, CLIENT_ID, CLIENT_SECRET
)
authorizer = prawcore.Authorizer(authenticator, REFRESH_TOKEN)
authorizer = prawcore.Authorizer(
authenticator, refresh_token=REFRESH_TOKEN
)
authorizer.refresh()
return authorizer

Expand Down

0 comments on commit bb0e002

Please sign in to comment.