diff options
author | Jonathan Huot <JonathanHuot@users.noreply.github.com> | 2018-09-21 23:40:59 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2018-09-21 23:40:59 +0200 |
commit | fabcf865a7f33e5592453189670a82f08dc4384b (patch) | |
tree | f0750c8ccb5239d13b42e3a38302c7ed52b43098 | |
parent | 326456cb78eb6b50e6f44f01cb0eaccc7652cf1f (diff) | |
parent | 127a3b58df22798bad9bb2e30743c2401c978d8a (diff) | |
download | oauthlib-fabcf865a7f33e5592453189670a82f08dc4384b.tar.gz |
Merge pull request #593 from jvanasco/fix-585_client_id
PR for #585, `client_id` behavior with `prepare_request_body`
-rw-r--r-- | CHANGELOG.rst | 10 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/backend_application.py | 15 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/base.py | 6 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/legacy_application.py | 13 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/service_application.py | 31 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/clients/web_application.py | 39 | ||||
-rw-r--r-- | oauthlib/oauth2/rfc6749/parameters.py | 49 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_backend_application.py | 1 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_legacy_application.py | 62 | ||||
-rw-r--r-- | tests/oauth2/rfc6749/clients/test_web_application.py | 82 |
10 files changed, 281 insertions, 27 deletions
diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a8e1941..fd53769 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,16 @@ Changelog ========= +Unreleased +------------------ + +* OAuth2's `prepare_token_request` supports sending an empty string for `client_id` (#585) +* OAuth2's `WebApplicationClient.prepare_request_body` was refactored to better + support sending or omitting the `client_id` via a new `include_client_id` kwarg. + By default this is included. The method will also emit a DeprecationWarning if + a `client_id` parameter is submitted; the already configured `self.client_id` + is the preferred option. (#585) + 2.1.0 (2018-05-21) ------------------ diff --git a/oauthlib/oauth2/rfc6749/clients/backend_application.py b/oauthlib/oauth2/rfc6749/clients/backend_application.py index cbad8b7..cd46f12 100644 --- a/oauthlib/oauth2/rfc6749/clients/backend_application.py +++ b/oauthlib/oauth2/rfc6749/clients/backend_application.py @@ -30,15 +30,26 @@ class BackendApplicationClient(Client): no additional authorization request is needed. """ - def prepare_request_body(self, body='', scope=None, **kwargs): + def prepare_request_body(self, body='', scope=None, + include_client_id=None, **kwargs): """Add the client credentials to the request body. The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format per `Appendix B`_ in the HTTP request entity-body: + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. + + :param include_client_id: `True` to send the `client_id` in the body of + the upstream request. Default `None`. This is + required if the client is not authenticating + with the authorization server as described + in `Section 3.2.1`_. + :type include_client_id: Boolean + :param kwargs: Extra credentials to include in the token request. The client MUST authenticate with the authorization server as @@ -56,5 +67,7 @@ class BackendApplicationClient(Client): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request('client_credentials', body=body, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/base.py b/oauthlib/oauth2/rfc6749/clients/base.py index 406832d..d8ded50 100644 --- a/oauthlib/oauth2/rfc6749/clients/base.py +++ b/oauthlib/oauth2/rfc6749/clients/base.py @@ -254,7 +254,8 @@ class Client(object): :param redirect_url: The redirect_url supplied with the authorization request (if there was one). - :param body: Request body (URL encoded string). + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param kwargs: Additional parameters to included in the request. @@ -286,7 +287,8 @@ class Client(object): :param refresh_token: Refresh token string. - :param body: Request body (URL encoded string). + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param scope: List of scopes to request. Must be equal to or a subset of the scopes granted when obtaining the refresh diff --git a/oauthlib/oauth2/rfc6749/clients/legacy_application.py b/oauthlib/oauth2/rfc6749/clients/legacy_application.py index b16fc9f..a13927a 100644 --- a/oauthlib/oauth2/rfc6749/clients/legacy_application.py +++ b/oauthlib/oauth2/rfc6749/clients/legacy_application.py @@ -38,7 +38,8 @@ class LegacyApplicationClient(Client): def __init__(self, client_id, **kwargs): super(LegacyApplicationClient, self).__init__(client_id, **kwargs) - def prepare_request_body(self, username, password, body='', scope=None, **kwargs): + def prepare_request_body(self, username, password, body='', scope=None, + include_client_id=None, **kwargs): """Add the resource owner password and username to the request body. The client makes a request to the token endpoint by adding the @@ -47,8 +48,16 @@ class LegacyApplicationClient(Client): :param username: The resource owner username. :param password: The resource owner password. + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. :param scope: The scope of the access request as described by `Section 3.3`_. + :param include_client_id: `True` to send the `client_id` in the body of + the upstream request. Default `None`. This is + required if the client is not authenticating + with the authorization server as described + in `Section 3.2.1`_. + :type include_client_id: Boolean :param kwargs: Extra credentials to include in the token request. If the client type is confidential or the client was issued client @@ -68,5 +77,7 @@ class LegacyApplicationClient(Client): .. _`Section 3.3`: https://tools.ietf.org/html/rfc6749#section-3.3 .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request('password', body=body, username=username, password=password, scope=scope, **kwargs) diff --git a/oauthlib/oauth2/rfc6749/clients/service_application.py b/oauthlib/oauth2/rfc6749/clients/service_application.py index 3045676..35333d8 100644 --- a/oauthlib/oauth2/rfc6749/clients/service_application.py +++ b/oauthlib/oauth2/rfc6749/clients/service_application.py @@ -72,7 +72,8 @@ class ServiceApplicationClient(Client): issued_at=None, extra_claims=None, body='', - scope=None, + scope=None, + include_client_id=None, **kwargs): """Create and add a JWT assertion to the request body. @@ -97,20 +98,32 @@ class ServiceApplicationClient(Client): :param issued_at: A unix timestamp of when the JWT was created. Defaults to now, i.e. ``time.time()``. - :param not_before: A unix timestamp after which the JWT may be used. - Not included unless provided. - - :param jwt_id: A unique JWT token identifier. Not included unless - provided. - :param extra_claims: A dict of additional claims to include in the JWT. + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. + :param scope: The scope of the access request. - :param body: Request body (string) with extra parameters. + :param include_client_id: `True` to send the `client_id` in the body of + the upstream request. Default `None`. This is + required if the client is not authenticating + with the authorization server as described + in `Section 3.2.1`_. + :type include_client_id: Boolean + + :param not_before: A unix timestamp after which the JWT may be used. + Not included unless provided. * + + :param jwt_id: A unique JWT token identifier. Not included unless + provided. * :param kwargs: Extra credentials to include in the token request. + Parameters marked with a `*` above are not explicit arguments in the + function signature, but are specially documented arguments for items + appearing in the generic `**kwargs` keyworded input. + The "scope" parameter may be used, as defined in the Assertion Framework for OAuth 2.0 Client Authentication and Authorization Grants [I-D.ietf-oauth-assertions] specification, to indicate the requested @@ -168,6 +181,8 @@ class ServiceApplicationClient(Client): assertion = jwt.encode(claim, key, 'RS256') assertion = to_unicode(assertion) + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request(self.grant_type, body=body, assertion=assertion, diff --git a/oauthlib/oauth2/rfc6749/clients/web_application.py b/oauthlib/oauth2/rfc6749/clients/web_application.py index c14a5f8..487e3a0 100644 --- a/oauthlib/oauth2/rfc6749/clients/web_application.py +++ b/oauthlib/oauth2/rfc6749/clients/web_application.py @@ -8,6 +8,8 @@ for consuming and providing OAuth 2.0 RFC6749. """ from __future__ import absolute_import, unicode_literals +import warnings + from ..parameters import (parse_authorization_code_response, parse_token_response, prepare_grant_uri, prepare_token_request) @@ -85,17 +87,14 @@ class WebApplicationClient(Client): return prepare_grant_uri(uri, self.client_id, 'code', redirect_uri=redirect_uri, scope=scope, state=state, **kwargs) - def prepare_request_body(self, client_id=None, code=None, body='', - redirect_uri=None, **kwargs): + def prepare_request_body(self, code=None, redirect_uri=None, body='', + include_client_id=True, **kwargs): """Prepare the access token request body. The client makes a request to the token endpoint by adding the following parameters using the "application/x-www-form-urlencoded" format in the HTTP request entity-body: - :param client_id: REQUIRED, if the client is not authenticating with the - authorization server as described in `Section 3.2.1`_. - :param code: REQUIRED. The authorization code received from the authorization server. @@ -103,6 +102,15 @@ class WebApplicationClient(Client): authorization request as described in `Section 4.1.1`_, and their values MUST be identical. + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. + + :param include_client_id: `True` (default) to send the `client_id` in the + body of the upstream request. This is required + if the client is not authenticating with the + authorization server as described in `Section 3.2.1`_. + :type include_client_id: Boolean + :param kwargs: Extra parameters to include in the token request. In addition OAuthLib will add the ``grant_type`` parameter set to @@ -120,12 +128,31 @@ class WebApplicationClient(Client): >>> client.prepare_request_body(code='sh35ksdf09sf', foo='bar') 'grant_type=authorization_code&code=sh35ksdf09sf&foo=bar' + `Section 3.2.1` also states: + In the "authorization_code" "grant_type" request to the token + endpoint, an unauthenticated client MUST send its "client_id" to + prevent itself from inadvertently accepting a code intended for a + client with a different "client_id". This protects the client from + substitution of the authentication code. (It provides no additional + security for the protected resource.) + .. _`Section 4.1.1`: https://tools.ietf.org/html/rfc6749#section-4.1.1 .. _`Section 3.2.1`: https://tools.ietf.org/html/rfc6749#section-3.2.1 """ code = code or self.code + if 'client_id' in kwargs: + warnings.warn("`client_id` has been deprecated in favor of " + "`include_client_id`, a boolean value which will " + "include the already configured `self.client_id`.", + DeprecationWarning) + if kwargs['client_id'] != self.client_id: + raise ValueError("`client_id` was supplied as an argument, but " + "it does not match `self.client_id`") + + kwargs['client_id'] = self.client_id + kwargs['include_client_id'] = include_client_id return prepare_token_request('authorization_code', code=code, body=body, - client_id=client_id, redirect_uri=redirect_uri, **kwargs) + redirect_uri=redirect_uri, **kwargs) def parse_request_uri_response(self, uri, state=None): """Parse the URI query for code and state. diff --git a/oauthlib/oauth2/rfc6749/parameters.py b/oauthlib/oauth2/rfc6749/parameters.py index 74a15f9..4d0baee 100644 --- a/oauthlib/oauth2/rfc6749/parameters.py +++ b/oauthlib/oauth2/rfc6749/parameters.py @@ -87,7 +87,7 @@ def prepare_grant_uri(uri, client_id, response_type, redirect_uri=None, return add_params_to_uri(uri, params) -def prepare_token_request(grant_type, body='', **kwargs): +def prepare_token_request(grant_type, body='', include_client_id=True, **kwargs): """Prepare the access token request. The client makes a request to the token endpoint by adding the @@ -96,14 +96,38 @@ def prepare_token_request(grant_type, body='', **kwargs): :param grant_type: To indicate grant type being used, i.e. "password", "authorization_code" or "client_credentials". - :param body: Existing request body to embed parameters in. - :param code: If using authorization code grant, pass the previously - obtained authorization code as the ``code`` argument. + + :param body: Existing request body (URL encoded string) to embed parameters + into. This may contain extra paramters. Default ''. + + :param include_client_id: `True` (default) to send the `client_id` in the + body of the upstream request. This is required + if the client is not authenticating with the + authorization server as described in + `Section 3.2.1`_. + :type include_client_id: Boolean + + :param client_id: Unicode client identifier. Will only appear if + `include_client_id` is True. * + + :param client_secret: Unicode client secret. Will only appear if set to a + value that is not `None`. Invoking this function with + an empty string will send an empty `client_secret` + value to the server. * + + :param code: If using authorization_code grant, pass the previously + obtained authorization code as the ``code`` argument. * + :param redirect_uri: If the "redirect_uri" parameter was included in the authorization request as described in - `Section 4.1.1`_, and their values MUST be identical. + `Section 4.1.1`_, and their values MUST be identical. * + :param kwargs: Extra arguments to embed in the request body. + Parameters marked with a `*` above are not explicit arguments in the + function signature, but are specially documented arguments for items + appearing in the generic `**kwargs` keyworded input. + An example of an authorization code token request body: .. code-block:: http @@ -118,6 +142,19 @@ def prepare_token_request(grant_type, body='', **kwargs): if 'scope' in kwargs: kwargs['scope'] = list_to_scope(kwargs['scope']) + # pull the `client_id` out of the kwargs. + client_id = kwargs.pop('client_id', None) + if include_client_id: + if client_id is not None: + params.append((unicode_type('client_id'), client_id)) + + # the kwargs iteration below only supports including boolean truth (truthy) + # values, but some servers may require an empty string for `client_secret` + client_secret = kwargs.pop('client_secret', None) + if client_secret is not None: + params.append((unicode_type('client_secret'), client_secret)) + + # this handles: `code`, `redirect_uri`, and other undocumented params for k in kwargs: if kwargs[k]: params.append((unicode_type(k), kwargs[k])) @@ -144,7 +181,7 @@ def prepare_token_revocation_request(url, token, token_type_hint="access_token", token types. An authorization server MAY ignore this parameter, particularly if it is able to detect the token type automatically. - + This specification defines two values for `token_type_hint`: * access_token: An access token as defined in [RFC6749], diff --git a/tests/oauth2/rfc6749/clients/test_backend_application.py b/tests/oauth2/rfc6749/clients/test_backend_application.py index 6b342f0..aa2ba2b 100644 --- a/tests/oauth2/rfc6749/clients/test_backend_application.py +++ b/tests/oauth2/rfc6749/clients/test_backend_application.py @@ -15,6 +15,7 @@ from ....unittest import TestCase class BackendApplicationClientTest(TestCase): client_id = "someclientid" + client_secret = 'someclientsecret' scope = ["/profile"] kwargs = { "some": "providers", diff --git a/tests/oauth2/rfc6749/clients/test_legacy_application.py b/tests/oauth2/rfc6749/clients/test_legacy_application.py index 3f97c02..21af4a3 100644 --- a/tests/oauth2/rfc6749/clients/test_legacy_application.py +++ b/tests/oauth2/rfc6749/clients/test_legacy_application.py @@ -10,19 +10,26 @@ from oauthlib.oauth2 import LegacyApplicationClient from ....unittest import TestCase +# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + @patch('time.time', new=lambda: 1000) class LegacyApplicationClientTest(TestCase): client_id = "someclientid" + client_secret = 'someclientsecret' scope = ["/profile"] kwargs = { "some": "providers", "require": "extra arguments" } - username = "foo" - password = "bar" + username = "user_username" + password = "user_password" body = "not=empty" body_up = "not=empty&grant_type=password&username=%s&password=%s" % (username, password) @@ -88,3 +95,54 @@ class LegacyApplicationClientTest(TestCase): finally: signals.scope_changed.disconnect(record_scope_change) del os.environ['OAUTHLIB_RELAX_TOKEN_SCOPE'] + + def test_prepare_request_body(self): + """ + see issue #585 + https://github.com/oauthlib/oauthlib/issues/585 + """ + client = LegacyApplicationClient(self.client_id) + + # scenario 1, default behavior to not include `client_id` + r1 = client.prepare_request_body(username=self.username, password=self.password) + self.assertIn(r1, ('grant_type=password&username=%s&password=%s' % (self.username, self.password, ), + 'grant_type=password&password=%s&username=%s' % (self.password, self.username, ), + )) + + # scenario 2, include `client_id` in the body + r2 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True) + r2_params = dict(urlparse.parse_qsl(r2, keep_blank_values=True)) + self.assertEqual(len(r2_params.keys()), 4) + self.assertEqual(r2_params['grant_type'], 'password') + self.assertEqual(r2_params['username'], self.username) + self.assertEqual(r2_params['password'], self.password) + self.assertEqual(r2_params['client_id'], self.client_id) + + # scenario 3, include `client_id` + `client_secret` in the body + r3 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=self.client_secret) + r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True)) + self.assertEqual(len(r3_params.keys()), 5) + self.assertEqual(r3_params['grant_type'], 'password') + self.assertEqual(r3_params['username'], self.username) + self.assertEqual(r3_params['password'], self.password) + self.assertEqual(r3_params['client_id'], self.client_id) + self.assertEqual(r3_params['client_secret'], self.client_secret) + + # scenario 4, `client_secret` is an empty string + r4 = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret='') + r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True)) + self.assertEqual(len(r4_params.keys()), 5) + self.assertEqual(r4_params['grant_type'], 'password') + self.assertEqual(r4_params['username'], self.username) + self.assertEqual(r4_params['password'], self.password) + self.assertEqual(r4_params['client_id'], self.client_id) + self.assertEqual(r4_params['client_secret'], '') + + # scenario 4b`,` client_secret is `None` + r4b = client.prepare_request_body(username=self.username, password=self.password, include_client_id=True, client_secret=None) + r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True)) + self.assertEqual(len(r4b_params.keys()), 4) + self.assertEqual(r4b_params['grant_type'], 'password') + self.assertEqual(r4b_params['username'], self.username) + self.assertEqual(r4b_params['password'], self.password) + self.assertEqual(r4b_params['client_id'], self.client_id) diff --git a/tests/oauth2/rfc6749/clients/test_web_application.py b/tests/oauth2/rfc6749/clients/test_web_application.py index 4ecc3b3..092f93e 100644 --- a/tests/oauth2/rfc6749/clients/test_web_application.py +++ b/tests/oauth2/rfc6749/clients/test_web_application.py @@ -3,6 +3,7 @@ from __future__ import absolute_import, unicode_literals import datetime import os +import warnings from mock import patch @@ -15,11 +16,18 @@ from oauthlib.oauth2.rfc6749.clients import AUTH_HEADER, BODY, URI_QUERY from ....unittest import TestCase +# this is the same import method used in oauthlib/oauth2/rfc6749/parameters.py +try: + import urlparse +except ImportError: + import urllib.parse as urlparse + @patch('time.time', new=lambda: 1000) class WebApplicationClientTest(TestCase): client_id = "someclientid" + client_secret = 'someclientsecret' uri = "https://example.com/path?query=world" uri_id = uri + "&response_type=code&client_id=" + client_id uri_redirect = uri_id + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback" @@ -38,7 +46,7 @@ class WebApplicationClientTest(TestCase): code = "zzzzaaaa" body = "not=empty" - body_code = "not=empty&grant_type=authorization_code&code=%s" % code + body_code = "not=empty&grant_type=authorization_code&code=%s&client_id=%s" % (code, client_id) body_redirect = body_code + "&redirect_uri=http%3A%2F%2Fmy.page.com%2Fcallback" body_kwargs = body_code + "&some=providers&require=extra+arguments" @@ -177,3 +185,75 @@ class WebApplicationClientTest(TestCase): # verify default header and body only self.assertEqual(header, {'Content-Type': 'application/x-www-form-urlencoded'}) self.assertEqual(body, '') + + def test_prepare_request_body(self): + """ + see issue #585 + https://github.com/oauthlib/oauthlib/issues/585 + + `prepare_request_body` should support the following scenarios: + 1. Include client_id alone in the body (default) + 2. Include client_id and client_secret in auth and not include them in the body (RFC preferred solution) + 3. Include client_id and client_secret in the body (RFC alternative solution) + 4. Include client_id in the body and an empty string for client_secret. + """ + client = WebApplicationClient(self.client_id) + + # scenario 1, default behavior to include `client_id` + r1 = client.prepare_request_body() + self.assertEqual(r1, 'grant_type=authorization_code&client_id=%s' % self.client_id) + + r1b = client.prepare_request_body(include_client_id=True) + self.assertEqual(r1b, 'grant_type=authorization_code&client_id=%s' % self.client_id) + + # scenario 2, do not include `client_id` in the body, so it can be sent in auth. + r2 = client.prepare_request_body(include_client_id=False) + self.assertEqual(r2, 'grant_type=authorization_code') + + # scenario 3, Include client_id and client_secret in the body (RFC alternative solution) + # the order of kwargs being appended is not guaranteed. for brevity, check the 2 permutations instead of sorting + r3 = client.prepare_request_body(client_secret=self.client_secret) + r3_params = dict(urlparse.parse_qsl(r3, keep_blank_values=True)) + self.assertEqual(len(r3_params.keys()), 3) + self.assertEqual(r3_params['grant_type'], 'authorization_code') + self.assertEqual(r3_params['client_id'], self.client_id) + self.assertEqual(r3_params['client_secret'], self.client_secret) + + r3b = client.prepare_request_body(include_client_id=True, client_secret=self.client_secret) + r3b_params = dict(urlparse.parse_qsl(r3b, keep_blank_values=True)) + self.assertEqual(len(r3b_params.keys()), 3) + self.assertEqual(r3b_params['grant_type'], 'authorization_code') + self.assertEqual(r3b_params['client_id'], self.client_id) + self.assertEqual(r3b_params['client_secret'], self.client_secret) + + # scenario 4, `client_secret` is an empty string + r4 = client.prepare_request_body(include_client_id=True, client_secret='') + r4_params = dict(urlparse.parse_qsl(r4, keep_blank_values=True)) + self.assertEqual(len(r4_params.keys()), 3) + self.assertEqual(r4_params['grant_type'], 'authorization_code') + self.assertEqual(r4_params['client_id'], self.client_id) + self.assertEqual(r4_params['client_secret'], '') + + # scenario 4b, `client_secret` is `None` + r4b = client.prepare_request_body(include_client_id=True, client_secret=None) + r4b_params = dict(urlparse.parse_qsl(r4b, keep_blank_values=True)) + self.assertEqual(len(r4b_params.keys()), 2) + self.assertEqual(r4b_params['grant_type'], 'authorization_code') + self.assertEqual(r4b_params['client_id'], self.client_id) + + # scenario Warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") # catch all + + # warning1 - raise a DeprecationWarning if a `client_id` is submitted + rWarnings1 = client.prepare_request_body(client_id=self.client_id) + self.assertEqual(len(w), 1) + self.assertIsInstance(w[0].message, DeprecationWarning) + + # testing the exact warning message in Python2&Python3 is a pain + + # scenario Exceptions + # exception1 - raise a ValueError if the a different `client_id` is submitted + with self.assertRaises(ValueError) as cm: + client.prepare_request_body(client_id='different_client_id') + # testing the exact exception message in Python2&Python3 is a pain |