1414
1515from boxsdk .exception import BoxOAuthException
1616from boxsdk .network .default_network import DefaultNetworkResponse
17- from boxsdk .auth .oauth2 import OAuth2
17+ from boxsdk .auth .oauth2 import OAuth2 , TokenScope
1818from boxsdk .config import API
19+ from boxsdk .object .file import File
20+ from boxsdk .object .folder import Folder
1921
2022
2123class MyError (Exception ):
@@ -197,12 +199,25 @@ def refresh_tokens_and_verify_the_response():
197199 thread .join ()
198200
199201
200- @pytest .mark .parametrize ('test_method' , [
201- partial (OAuth2 .refresh , access_token_to_refresh = 'fake_access_token' ),
202- partial (OAuth2 .authenticate , auth_code = 'fake_code' )
203- ])
202+ @pytest .fixture ()
203+ def token_method (request , mock_box_session , mock_object_id ):
204+ """ Fixture that returns a partial method based on the method provided in request.param"""
205+ if request .param == OAuth2 .refresh :
206+ return partial (OAuth2 .refresh , access_token_to_refresh = 'fake_access_token' )
207+ elif request .param == OAuth2 .authenticate :
208+ return partial (OAuth2 .authenticate , auth_code = 'fake_code' )
209+ elif request .param == OAuth2 .downscope_token :
210+ item = File (mock_box_session , mock_object_id )
211+ return partial (OAuth2 .downscope_token , scopes = [TokenScope .ITEM_READ ], item = item )
212+
213+
214+ @pytest .mark .parametrize (
215+ 'token_method' ,
216+ [OAuth2 .refresh , OAuth2 .authenticate , OAuth2 .downscope_token ],
217+ indirect = True ,
218+ )
204219def test_token_request_raises_box_oauth_exception_when_getting_bad_network_response (
205- test_method ,
220+ token_method ,
206221 mock_network_layer ,
207222 bad_network_response ,
208223):
@@ -214,15 +229,16 @@ def test_token_request_raises_box_oauth_exception_when_getting_bad_network_respo
214229 access_token = 'fake_access_token' ,
215230 network_layer = mock_network_layer ,
216231 )
217- test_method (oauth )
232+ token_method (oauth )
218233
219234
220- @pytest .mark .parametrize ('test_method' , [
221- partial (OAuth2 .refresh , access_token_to_refresh = 'fake_access_token' ),
222- partial (OAuth2 .authenticate , auth_code = 'fake_code' )
223- ])
235+ @pytest .mark .parametrize (
236+ 'token_method' ,
237+ [OAuth2 .refresh , OAuth2 .authenticate , OAuth2 .downscope_token ],
238+ indirect = True ,
239+ )
224240def test_token_request_raises_box_oauth_exception_when_no_json_object_can_be_decoded (
225- test_method ,
241+ token_method ,
226242 mock_network_layer ,
227243 non_json_response ,
228244):
@@ -234,7 +250,7 @@ def test_token_request_raises_box_oauth_exception_when_no_json_object_can_be_dec
234250 network_layer = mock_network_layer ,
235251 )
236252 with pytest .raises (BoxOAuthException ):
237- test_method (oauth )
253+ token_method (oauth )
238254
239255
240256@pytest .fixture (params = [
@@ -287,6 +303,17 @@ def test_token_request_allows_missing_refresh_token(mock_network_layer):
287303 oauth .send_token_request ({}, access_token = None , expect_refresh_token = False )
288304
289305
306+ @pytest .fixture ()
307+ def oauth (client_id , client_secret , access_token , refresh_token , mock_network_layer ):
308+ return OAuth2 (
309+ client_id = client_id ,
310+ client_secret = client_secret ,
311+ access_token = access_token ,
312+ refresh_token = refresh_token ,
313+ network_layer = mock_network_layer ,
314+ )
315+
316+
290317@pytest .mark .parametrize (
291318 'access_token,refresh_token,expected_token_to_revoke' ,
292319 (
@@ -299,19 +326,12 @@ def test_revoke_sends_revoke_request(
299326 client_secret ,
300327 mock_network_layer ,
301328 access_token ,
302- refresh_token ,
329+ oauth ,
303330 expected_token_to_revoke ,
304331):
305332 mock_network_response = Mock ()
306333 mock_network_response .ok = True
307334 mock_network_layer .request .return_value = mock_network_response
308- oauth = OAuth2 (
309- client_id = client_id ,
310- client_secret = client_secret ,
311- access_token = access_token ,
312- refresh_token = refresh_token ,
313- network_layer = mock_network_layer ,
314- )
315335 oauth .revoke ()
316336 mock_network_layer .request .assert_called_once_with (
317337 'POST' ,
@@ -326,6 +346,84 @@ def test_revoke_sends_revoke_request(
326346 assert oauth .access_token is None
327347
328348
349+ @pytest .mark .parametrize (
350+ 'item_class,scopes,expected_scopes' ,
351+ [
352+ (File , [TokenScope .ITEM_READWRITE ], 'item_readwrite' ),
353+ (Folder , [TokenScope .ITEM_PREVIEW , TokenScope .ITEM_SHARE ], 'item_preview item_share' ),
354+ (File , [TokenScope .ITEM_READ , TokenScope .ITEM_SHARE , TokenScope .ITEM_DELETE ], 'item_read item_share item_delete' ),
355+ (None , [TokenScope .ITEM_DOWNLOAD ], 'item_download' ),
356+ ],
357+ )
358+ def test_downscope_token_sends_downscope_request (
359+ oauth ,
360+ access_token ,
361+ mock_network_layer ,
362+ mock_box_session ,
363+ mock_object_id ,
364+ make_mock_box_request ,
365+ item_class ,
366+ scopes ,
367+ expected_scopes ,
368+ ):
369+ mock_downscoped_token = 'mock_downscoped_token'
370+ mock_network_response , _ = make_mock_box_request (response = {'access_token' : mock_downscoped_token })
371+ mock_network_layer .request .return_value = mock_network_response
372+
373+ item = item_class (mock_box_session , mock_object_id ) if item_class else None
374+ downscoped_token = oauth .downscope_token (scopes , item )
375+
376+ assert downscoped_token == mock_downscoped_token
377+ expected_data = {
378+ 'subject_token' : access_token ,
379+ 'subject_token_type' : 'urn:ietf:params:oauth:token-type:access_token' ,
380+ 'scope' : expected_scopes ,
381+ 'grant_type' : 'urn:ietf:params:oauth:grant-type:token-exchange' ,
382+ }
383+ if item :
384+ expected_data ['resource' ] = item .get_url ()
385+ mock_network_layer .request .assert_called_once_with (
386+ 'POST' ,
387+ '{0}/token' .format (API .OAUTH2_API_URL ),
388+ data = expected_data ,
389+ headers = {'content-type' : 'application/x-www-form-urlencoded' },
390+ access_token = access_token ,
391+ )
392+
393+
394+ def test_downscope_token_sends_downscope_request_with_additional_data (
395+ oauth ,
396+ access_token ,
397+ mock_network_layer ,
398+ mock_box_session ,
399+ mock_object_id ,
400+ make_mock_box_request ,
401+ ):
402+ mock_downscoped_token = 'mock_downscoped_token'
403+ mock_network_response , _ = make_mock_box_request (response = {'access_token' : mock_downscoped_token })
404+ mock_network_layer .request .return_value = mock_network_response
405+
406+ item = File (mock_box_session , mock_object_id )
407+ additional_data = {'grant_type' : 'new_grant_type' , 'extra_data_key' : 'extra_data_value' }
408+ downscoped_token = oauth .downscope_token ([TokenScope .ITEM_READWRITE ], item , additional_data )
409+
410+ assert downscoped_token == mock_downscoped_token
411+ mock_network_layer .request .assert_called_once_with (
412+ 'POST' ,
413+ '{0}/token' .format (API .OAUTH2_API_URL ),
414+ data = {
415+ 'subject_token' : access_token ,
416+ 'subject_token_type' : 'urn:ietf:params:oauth:token-type:access_token' ,
417+ 'scope' : 'item_readwrite' ,
418+ 'resource' : item .get_url (),
419+ 'grant_type' : 'new_grant_type' ,
420+ 'extra_data_key' : 'extra_data_value' ,
421+ },
422+ headers = {'content-type' : 'application/x-www-form-urlencoded' },
423+ access_token = access_token ,
424+ )
425+
426+
329427def test_tokens_get_updated_after_noop_refresh (client_id , client_secret , access_token , new_access_token , refresh_token , mock_network_layer ):
330428 """`OAuth2` object should update its state with new tokens, after no-op refresh.
331429
0 commit comments