summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.travis.yml2
-rw-r--r--AUTHORS1
-rw-r--r--oauthlib/oauth2/rfc6749/clients/backend_application.py26
-rw-r--r--oauthlib/oauth2/rfc6749/clients/legacy_application.py26
-rw-r--r--oauthlib/oauth2/rfc6749/clients/mobile_application.py18
-rw-r--r--oauthlib/oauth2/rfc6749/clients/web_application.py26
-rw-r--r--oauthlib/oauth2/rfc6749/parameters.py15
-rw-r--r--oauthlib/signals.py41
-rwxr-xr-xsetup.py12
-rw-r--r--tests/oauth2/rfc6749/clients/test_backend_application.py16
-rw-r--r--tests/oauth2/rfc6749/clients/test_legacy_application.py16
-rw-r--r--tests/oauth2/rfc6749/clients/test_mobile_application.py16
-rw-r--r--tests/oauth2/rfc6749/clients/test_web_application.py17
-rw-r--r--tests/oauth2/rfc6749/test_parameters.py33
14 files changed, 199 insertions, 66 deletions
diff --git a/.travis.yml b/.travis.yml
index 8d9c572..f8c39b4 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -9,7 +9,7 @@ python:
install:
- if [[ $TRAVIS_PYTHON_VERSION == '2.6' ]]; then pip install --use-mirrors unittest2; fi
- - pip install nose pycrypto mock pyjwt
+ - pip install nose pycrypto mock pyjwt blinker
script:
- nosetests -w tests
diff --git a/AUTHORS b/AUTHORS
index cffaf99..da767fe 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -21,3 +21,4 @@ Kyle Valade
Tyler Jones (squirly)
Massimiliano Pippi
Josh Turmel
+David Baumgold
diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py
index 9e0d438..103fe6f 100644
--- a/oauthlib/oauth2/rfc6749/clients/backend_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py
@@ -72,7 +72,7 @@ class BackendApplicationClient(Client):
:param body: The response body from the token request.
:param scope: Scopes originally requested.
:return: Dictionary of token parameters.
- :raises: Warning if scope has changed. OAuth2Error if response is invalid.
+ :raises: OAuth2Error if response is invalid.
These response are json encoded and could easily be parsed without
the assistance of OAuthLib. However, there are a few subtle issues
@@ -123,18 +123,19 @@ class BackendApplicationClient(Client):
'scope': ['hello', 'world'], # note the list
}
- If there was a scope change you will be notified with a warning::
-
+ If there was a scope change, a "scope changed" signal will be dispatched
+ using the `blinker`_ library, if installed. (If blinker is not installed,
+ there will be no notification on scope change.) To be automatically
+ notified when the returned scope is different from the requested scope,
+ simply connect a function to the ``oauthlib.signals.scope_changed``
+ dispatcher::
+
+ >>> def alert_scope_changed(message, old, new):
+ ... print(message, old, new)
+ ...
+ >>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
>>> client.parse_request_body_response(response_body, scope=['images'])
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response
- .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
- File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response
- validate_token_parameters(params, scope)
- File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters
- raise Warning("Scope has changed to %s." % new_scope)
- Warning: Scope has changed to [u'hello', u'world'].
+ ('Scope has changed from "images" to "hello world".', ['images'], ['hello', 'world'])
If there was an error on the providers side you will be notified with
an error. For example, if there was no ``token_type`` provided::
@@ -153,6 +154,7 @@ class BackendApplicationClient(Client):
.. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`blinker`: http://pythonhosted.org/blinker/
"""
self.token = parse_token_response(body, scope=scope)
self._populate_attributes(self.token)
diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py
index edb68d7..2632d06 100644
--- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py
@@ -84,7 +84,7 @@ class LegacyApplicationClient(Client):
:param body: The response body from the token request.
:param scope: Scopes originally requested.
:return: Dictionary of token parameters.
- :raises: Warning if scope has changed. OAuth2Error if response is invalid.
+ :raises: OAuth2Error if response is invalid.
These response are json encoded and could easily be parsed without
the assistance of OAuthLib. However, there are a few subtle issues
@@ -135,18 +135,19 @@ class LegacyApplicationClient(Client):
'scope': ['hello', 'world'], # note the list
}
- If there was a scope change you will be notified with a warning::
-
+ If there was a scope change, a "scope changed" signal will be dispatched
+ using the `blinker`_ library, if installed. (If blinker is not installed,
+ there will be no notification on scope change.) To be automatically
+ notified when the returned scope is different from the requested scope,
+ simply connect a function to the ``oauthlib.signals.scope_changed``
+ dispatcher::
+
+ >>> def alert_scope_changed(message, old, new):
+ ... print(message, old, new)
+ ...
+ >>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
>>> client.parse_request_body_response(response_body, scope=['images'])
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response
- .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
- File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response
- validate_token_parameters(params, scope)
- File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters
- raise Warning("Scope has changed to %s." % new_scope)
- Warning: Scope has changed to [u'hello', u'world'].
+ ('Scope has changed from "images" to "hello world".', ['images'], ['hello', 'world'])
If there was an error on the providers side you will be notified with
an error. For example, if there was no ``token_type`` provided::
@@ -165,6 +166,7 @@ class LegacyApplicationClient(Client):
.. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`blinker`: http://pythonhosted.org/blinker/
"""
self.token = parse_token_response(body, scope=scope)
self._populate_attributes(self.token)
diff --git a/oauthlib/oauth2/rfc6749/clients/mobile_application.py b/oauthlib/oauth2/rfc6749/clients/mobile_application.py
index 6b79c81..9e3ef21 100644
--- a/oauthlib/oauth2/rfc6749/clients/mobile_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/mobile_application.py
@@ -108,7 +108,7 @@ class MobileApplicationClient(Client):
:param state: The state provided in the authorization request.
:param scope: The scopes provided in the authorization request.
:return: Dictionary of token parameters.
- :raises: Warning if scope has changed. OAuth2Error if response is invalid.
+ :raises: OAuth2Error if response is invalid.
A successful response should always contain
@@ -158,16 +158,12 @@ class MobileApplicationClient(Client):
File "oauthlib/oauth2/rfc6749/parameters.py", line 197, in parse_implicit_response
raise ValueError("Mismatching or missing state in params.")
ValueError: Mismatching or missing state in params.
- >>> client.parse_request_uri_response(response_uri, scope=['other'])
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "oauthlib/oauth2/rfc6749/__init__.py", line 598, in parse_request_uri_response
- **scope**
- File "oauthlib/oauth2/rfc6749/parameters.py", line 199, in parse_implicit_response
- validate_token_parameters(params, scope)
- File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters
- raise Warning("Scope has changed to %s." % new_scope)
- Warning: Scope has changed to [u'hello', u'world'].
+ >>> def alert_scope_changed(message, old, new):
+ ... print(message, old, new)
+ ...
+ >>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
+ >>> client.parse_request_body_response(response_body, scope=['other'])
+ ('Scope has changed from "other" to "hello world".', ['other'], ['hello', 'world'])
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
.. _`Section 3.3`: http://tools.ietf.org/html/rfc6749#section-3.3
diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py
index 6aeb831..941b0e9 100644
--- a/oauthlib/oauth2/rfc6749/clients/web_application.py
+++ b/oauthlib/oauth2/rfc6749/clients/web_application.py
@@ -187,7 +187,7 @@ class WebApplicationClient(Client):
:param body: The response body from the token request.
:param scope: Scopes originally requested.
:return: Dictionary of token parameters.
- :raises: Warning if scope has changed. OAuth2Error if response is invalid.
+ :raises: OAuth2Error if response is invalid.
These response are json encoded and could easily be parsed without
the assistance of OAuthLib. However, there are a few subtle issues
@@ -238,18 +238,19 @@ class WebApplicationClient(Client):
'scope': ['hello', 'world'], # note the list
}
- If there was a scope change you will be notified with a warning::
-
+ If there was a scope change, a "scope changed" signal will be dispatched
+ using the `blinker`_ library, if installed. (If blinker is not installed,
+ there will be no notification on scope change.) To be automatically
+ notified when the returned scope is different from the requested scope,
+ simply connect a function to the ``oauthlib.signals.scope_changed``
+ dispatcher::
+
+ >>> def alert_scope_changed(message, old, new):
+ ... print(message, old, new)
+ ...
+ >>> oauthlib.signals.scope_changed.connect(alert_scope_changed)
>>> client.parse_request_body_response(response_body, scope=['images'])
- Traceback (most recent call last):
- File "<stdin>", line 1, in <module>
- File "oauthlib/oauth2/rfc6749/__init__.py", line 421, in parse_request_body_response
- .. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
- File "oauthlib/oauth2/rfc6749/parameters.py", line 263, in parse_token_response
- validate_token_parameters(params, scope)
- File "oauthlib/oauth2/rfc6749/parameters.py", line 285, in validate_token_parameters
- raise Warning("Scope has changed to %s." % new_scope)
- Warning: Scope has changed to [u'hello', u'world'].
+ ('Scope has changed from "images" to "hello world".', ['images'], ['hello', 'world'])
If there was an error on the providers side you will be notified with
an error. For example, if there was no ``token_type`` provided::
@@ -268,6 +269,7 @@ class WebApplicationClient(Client):
.. _`Section 5.1`: http://tools.ietf.org/html/rfc6749#section-5.1
.. _`Section 5.2`: http://tools.ietf.org/html/rfc6749#section-5.2
.. _`Section 7.1`: http://tools.ietf.org/html/rfc6749#section-7.1
+ .. _`blinker`: http://pythonhosted.org/blinker/
"""
self.token = parse_token_response(body, scope=scope)
self._populate_attributes(self.token)
diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py
index 50269ad..831f84d 100644
--- a/oauthlib/oauth2/rfc6749/parameters.py
+++ b/oauthlib/oauth2/rfc6749/parameters.py
@@ -17,6 +17,7 @@ try:
except ImportError:
import urllib.parse as urlparse
from oauthlib.common import add_params_to_uri, add_params_to_qs, unicode_type
+from oauthlib.signals import scope_changed
from .errors import raise_from_error, MissingTokenError, MissingTokenTypeError
from .errors import MismatchingStateError, MissingCodeError
from .errors import InsecureTransportError
@@ -390,11 +391,9 @@ def validate_token_parameters(params, scope=None):
# parameter to inform the client of the actual scope granted.
# http://tools.ietf.org/html/rfc6749#section-3.3
new_scope = params.get('scope', None)
- scope = scope_to_list(scope)
- if scope and new_scope and set(scope) != set(new_scope):
- w = Warning("Scope has changed from {old} to {new}.".format(
- old=scope, new=new_scope,
- ))
- w.old_scope = scope
- w.new_scope = new_scope
- raise w
+ old_scope = scope_to_list(scope)
+ if old_scope and new_scope and set(old_scope) != set(new_scope):
+ message = 'Scope has changed from "{old}" to "{new}".'.format(
+ old=scope, new=list_to_scope(new_scope),
+ )
+ scope_changed.send(message=message, old=old_scope, new=new_scope)
diff --git a/oauthlib/signals.py b/oauthlib/signals.py
new file mode 100644
index 0000000..2f86650
--- /dev/null
+++ b/oauthlib/signals.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+"""
+ Implements signals based on blinker if available, otherwise
+ falls silently back to a noop. Shamelessly stolen from flask.signals:
+ https://github.com/mitsuhiko/flask/blob/master/flask/signals.py
+"""
+signals_available = False
+try:
+ from blinker import Namespace
+ signals_available = True
+except ImportError:
+ class Namespace(object):
+ def signal(self, name, doc=None):
+ return _FakeSignal(name, doc)
+
+ class _FakeSignal(object):
+ """If blinker is unavailable, create a fake class with the same
+ interface that allows sending of signals but will fail with an
+ error on anything else. Instead of doing anything on send, it
+ will just ignore the arguments and do nothing instead.
+ """
+
+ def __init__(self, name, doc=None):
+ self.name = name
+ self.__doc__ = doc
+ def _fail(self, *args, **kwargs):
+ raise RuntimeError('signalling support is unavailable '
+ 'because the blinker library is '
+ 'not installed.')
+ send = lambda *a, **kw: None
+ connect = disconnect = has_receivers_for = receivers_for = \
+ temporarily_connected_to = connected_to = _fail
+ del _fail
+
+# The namespace for code signals. If you are not oauthlib code, do
+# not put signals in here. Create your own namespace instead.
+_signals = Namespace()
+
+
+# Core signals.
+scope_changed = _signals.signal('scope-changed')
diff --git a/setup.py b/setup.py
index 91daa35..da76450 100755
--- a/setup.py
+++ b/setup.py
@@ -18,11 +18,12 @@ def fread(fn):
return f.read()
if sys.version_info[0] == 3:
- tests_require = ['nose', 'pycrypto', 'pyjwt']
+ tests_require = ['nose', 'pycrypto', 'pyjwt', 'blinker']
else:
- tests_require = ['nose', 'unittest2', 'pycrypto', 'mock', 'pyjwt']
+ tests_require = ['nose', 'unittest2', 'pycrypto', 'mock', 'pyjwt', 'blinker']
rsa_require = ['pycrypto']
signedtoken_require = ['pycrypto', 'pyjwt']
+signals_require = ['blinker']
requires = []
@@ -41,7 +42,12 @@ setup(
packages=find_packages(exclude=('docs', 'tests', 'tests.*')),
test_suite='nose.collector',
tests_require=tests_require,
- extras_require={'test': tests_require, 'rsa': rsa_require, 'signedtoken': signedtoken_require},
+ extras_require={
+ 'test': tests_require,
+ 'rsa': rsa_require,
+ 'signedtoken': signedtoken_require,
+ 'signals': signals_require,
+ },
install_requires=requires,
classifiers=[
'Development Status :: 4 - Beta',
diff --git a/tests/oauth2/rfc6749/clients/test_backend_application.py b/tests/oauth2/rfc6749/clients/test_backend_application.py
index 19ff1ef..ad6f9f2 100644
--- a/tests/oauth2/rfc6749/clients/test_backend_application.py
+++ b/tests/oauth2/rfc6749/clients/test_backend_application.py
@@ -4,6 +4,7 @@ from __future__ import absolute_import, unicode_literals
from mock import patch
from oauthlib.oauth2 import BackendApplicationClient
+from oauthlib import signals
from ....unittest import TestCase
@@ -63,4 +64,17 @@ class BackendApplicationClientTest(TestCase):
self.assertEqual(client.token_type, response.get("token_type"))
# Mismatching state
- self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid")
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py
index 2af2e6c..a0ed642 100644
--- a/tests/oauth2/rfc6749/clients/test_legacy_application.py
+++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
from mock import patch
+from oauthlib import signals
from oauthlib.oauth2 import LegacyApplicationClient
from ....unittest import TestCase
@@ -65,4 +66,17 @@ class LegacyApplicationClientTest(TestCase):
self.assertEqual(client.token_type, response.get("token_type"))
# Mismatching state
- self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid")
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
diff --git a/tests/oauth2/rfc6749/clients/test_mobile_application.py b/tests/oauth2/rfc6749/clients/test_mobile_application.py
index 7c57101..1186946 100644
--- a/tests/oauth2/rfc6749/clients/test_mobile_application.py
+++ b/tests/oauth2/rfc6749/clients/test_mobile_application.py
@@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals
from mock import patch
+from oauthlib import signals
from oauthlib.oauth2 import MobileApplicationClient
from ....unittest import TestCase
@@ -77,4 +78,17 @@ class MobileApplicationClientTest(TestCase):
self.assertEqual(client.token_type, response.get("token_type"))
# Mismatching scope
- self.assertRaises(Warning, client.parse_request_uri_response, self.response_uri, scope="invalid")
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_uri_response(self.response_uri, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py
index 0eac008..6f7b7e1 100644
--- a/tests/oauth2/rfc6749/clients/test_web_application.py
+++ b/tests/oauth2/rfc6749/clients/test_web_application.py
@@ -5,7 +5,7 @@ import datetime
from mock import patch
-from oauthlib import common
+from oauthlib import common, signals
from oauthlib.oauth2.rfc6749 import utils, errors
from oauthlib.oauth2 import Client
from oauthlib.oauth2 import WebApplicationClient
@@ -128,4 +128,17 @@ class WebApplicationClientTest(TestCase):
self.assertEqual(client.token_type, response.get("token_type"))
# Mismatching state
- self.assertRaises(Warning, client.parse_request_body_response, self.token_json, scope="invalid")
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ client.parse_request_body_response(self.token_json, scope="invalid")
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "invalid" to "/profile".')
+ self.assertEqual(old, ['invalid'])
+ self.assertEqual(new, ['/profile'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
diff --git a/tests/oauth2/rfc6749/test_parameters.py b/tests/oauth2/rfc6749/test_parameters.py
index c411fee..5122293 100644
--- a/tests/oauth2/rfc6749/test_parameters.py
+++ b/tests/oauth2/rfc6749/test_parameters.py
@@ -5,6 +5,7 @@ from mock import patch
from ...unittest import TestCase
from oauthlib.oauth2.rfc6749.parameters import *
from oauthlib.oauth2.rfc6749.errors import *
+from oauthlib import signals
@patch('time.time', new=lambda: 1000)
@@ -193,7 +194,21 @@ class ParameterTests(TestCase):
self.assertEqual(parse_token_response(self.json_response), self.json_dict)
self.assertRaises(InvalidRequestError, parse_token_response, self.json_error)
self.assertRaises(MissingTokenError, parse_token_response, self.json_notoken)
- self.assertRaises(Warning, parse_token_response, self.json_response, scope='aaa')
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ parse_token_response(self.json_response, scope='aaa')
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "aaa" to "abc def".')
+ self.assertEqual(old, ['aaa'])
+ self.assertEqual(new, ['abc', 'def'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
def test_json_token_notype(self):
"""Verify strict token type parsing only when configured. """
@@ -209,7 +224,21 @@ class ParameterTests(TestCase):
self.assertEqual(parse_token_response(self.url_encoded_response), self.json_dict)
self.assertRaises(InvalidRequestError, parse_token_response, self.url_encoded_error)
self.assertRaises(MissingTokenError, parse_token_response, self.url_encoded_notoken)
- self.assertRaises(Warning, parse_token_response, self.url_encoded_response, scope='aaa')
+
+ scope_changes_recorded = []
+ def record_scope_change(sender, message, old, new):
+ scope_changes_recorded.append((message, old, new))
+
+ signals.scope_changed.connect(record_scope_change)
+ try:
+ parse_token_response(self.url_encoded_response, scope='aaa')
+ self.assertEqual(len(scope_changes_recorded), 1)
+ message, old, new = scope_changes_recorded[0]
+ self.assertEqual(message, 'Scope has changed from "aaa" to "abc def".')
+ self.assertEqual(old, ['aaa'])
+ self.assertEqual(new, ['abc', 'def'])
+ finally:
+ signals.scope_changed.disconnect(record_scope_change)
def test_token_response_with_expires(self):
"""Verify fallback for alternate spelling of expires_in. """