11import os
2+ import time
23import json
34import pytest
45from requests_oauthlib import OAuth2Session
6+ from requests import HTTPError
57
6- from nypl_py_utils import Oauth2ApiClient
7- # from requests.exceptions import ConnectTimeout
8+ from nypl_py_utils import (Oauth2ApiClient , Oauth2ApiClientError )
89
910_TOKEN_RESPONSE = {
1011 'access_token' : 'super-secret-token' ,
11- 'expires_in' : 3600 ,
12+ 'expires_in' : 1 ,
1213 'token_type' : 'Bearer' ,
1314 'scope' : ['offline_access' , 'openid' , 'login:staff' , 'admin' ],
14- 'id_token' : 'super-secret-token' ,
15- 'expires_at' : 1677599823.3180869
15+ 'id_token' : 'super-secret-token'
1616}
1717
1818BASE_URL = 'https://example.com/api/v0.1'
19+ TOKEN_URL = 'https://oauth.example.com/oauth/token'
1920
2021
2122class TestOauth2ApiClient :
2223
2324 @pytest .fixture
24- def test_instance (self , requests_mock ):
25- token_url = 'https://oauth.example.com/oauth/token'
26- requests_mock .post (token_url , text = json .dumps (_TOKEN_RESPONSE ))
25+ def token_server_post (self , requests_mock ):
26+ token_url = TOKEN_URL
27+ token_response = dict (_TOKEN_RESPONSE )
28+ return requests_mock .post (token_url , text = json .dumps (token_response ))
2729
30+ @pytest .fixture
31+ def test_instance (self , requests_mock ):
2832 return Oauth2ApiClient (base_url = BASE_URL ,
29- token_url = token_url ,
33+ token_url = TOKEN_URL ,
3034 client_id = 'clientid' ,
3135 client_secret = 'clientsecret'
3236 )
@@ -50,42 +54,84 @@ def test_uses_env_vars(self):
5054 for key , value in env .items ():
5155 os .environ [key ] = ''
5256
53- def test_generate_access_token (self , test_instance ):
57+ def test_generate_access_token (self , test_instance , token_server_post ):
58+ test_instance ._create_oauth_client ()
5459 test_instance ._generate_access_token ()
55- assert test_instance .token ['access_token' ]\
60+ assert test_instance .oauth_client . token ['access_token' ]\
5661 == _TOKEN_RESPONSE ['access_token' ]
5762
58- def test_create_oauth_client (self , test_instance ):
63+ def test_create_oauth_client (self , token_server_post , test_instance ):
5964 test_instance ._create_oauth_client ()
6065 assert type (test_instance .oauth_client ) is OAuth2Session
6166
62- def test_do_http_method (self , requests_mock , test_instance ):
67+ def test_do_http_method (self , requests_mock , token_server_post ,
68+ test_instance ):
69+ requests_mock .get (f'{ BASE_URL } /foo' , json = {'foo' : 'bar' })
70+
6371 requests_mock .get (f'{ BASE_URL } /foo' , json = {'foo' : 'bar' })
6472 resp = test_instance ._do_http_method ('GET' , 'foo' )
65- assert resp == {'foo' : 'bar' }
73+ assert resp .status_code == 200
74+ assert resp .json () == {'foo' : 'bar' }
6675
67- def test_token_expiration (self , requests_mock , test_instance ):
76+ def test_token_expiration (self , requests_mock , test_instance ,
77+ token_server_post , mocker ):
6878 api_get_mock = requests_mock .get (f'{ BASE_URL } /foo' ,
6979 json = {'foo' : 'bar' })
7080
71- # Perform first request to auto-authenticate:
72- resp = test_instance ._do_http_method ('GET' , 'foo' )
81+ # Perform first request:
82+ test_instance ._do_http_method ('GET' , 'foo' )
83+ # Expect this first call triggered a single token server call:
84+ assert len (token_server_post .request_history ) == 1
85+ # And the GET request used the supplied Bearer token:
7386 assert api_get_mock .request_history [0 ]._request \
7487 .headers ['Authorization' ] == 'Bearer super-secret-token'
7588
76- # Emulate token expiration:
77- test_instance .token ['expires_at' ] = 0
78-
79- token_post_mock = requests_mock .post (
80- test_instance .token_url ,
81- text = json .dumps (_TOKEN_RESPONSE )
82- )
89+ # The token obtained above expires in 1s, so wait out expiration:
90+ time .sleep (1.1 )
91+
92+ # Register new token response:
93+ second_token_response = dict (_TOKEN_RESPONSE )
94+ second_token_response ['id_token' ] = 'super-secret-second-token'
95+ second_token_response ['access_token' ] = 'super-secret-second-token'
96+ second_token_server_post = requests_mock \
97+ .post (TOKEN_URL , text = json .dumps (second_token_response ))
98+
99+ # Perform second request:
100+ test_instance ._do_http_method ('GET' , 'foo' )
101+ # Expect a call on the second token server:
102+ assert len (second_token_server_post .request_history ) == 1
103+ # Expect the second GET request to carry the new Bearer token:
104+ assert api_get_mock .request_history [1 ]._request \
105+ .headers ['Authorization' ] == 'Bearer super-secret-second-token'
106+
107+ def test_error_status_raises_error (self , requests_mock , test_instance ,
108+ token_server_post ):
109+ requests_mock .get (f'{ BASE_URL } /foo' , status_code = 400 )
110+
111+ with pytest .raises (HTTPError ):
112+ test_instance ._do_http_method ('GET' , 'foo' )
113+
114+ def test_token_refresh_failure_raises_error (
115+ self , requests_mock , test_instance , token_server_post ):
116+ """
117+ Failure to fetch a token can raise a number of errors including:
118+ - requests.exceptions.HTTPError for invalid access_token
119+ - oauthlib.oauth2.rfc6749.errors.InvalidClientError for invalid
120+ credentials
121+ - oauthlib.oauth2.rfc6749.errors.MissingTokenError for failure to
122+ fetch a token
123+ One error that can arise from this client itself is failure to fetch
124+ a new valid token in response to token expiration. This test asserts
125+ that the client will not allow more than successive 3 retries.
126+ """
127+ requests_mock .get (f'{ BASE_URL } /foo' , json = {'foo' : 'bar' })
83128
84- # Perform second request, which should detect token expiration and
85- # re-authenticate:
86- resp = test_instance ._do_http_method ('GET' , 'foo' )
129+ token_response = dict (_TOKEN_RESPONSE )
130+ token_response ['expires_in' ] = 0
131+ token_server_post = requests_mock \
132+ .post (TOKEN_URL , text = json .dumps (token_response ))
87133
88- assert token_post_mock . called is True
89- assert api_get_mock . request_history [ 1 ]. _request \
90- . headers [ 'Authorization' ] == 'Bearer super-secret-token'
91- assert resp == { 'foo' : 'bar' }
134+ with pytest . raises ( Oauth2ApiClientError ):
135+ test_instance . _do_http_method ( 'GET' , 'foo' )
136+ # Expect 1 initial token fetch, plus 3 retries:
137+ assert len ( token_server_post . request_history ) == 4
0 commit comments