1+ import os
2+ import sys
3+
4+ # Add the parent directory (examples/authentication) to sys.path
5+ # to allow importing generate_user_credentials
6+ current_dir = os .path .dirname (os .path .abspath (__file__ ))
7+ parent_dir = os .path .dirname (current_dir )
8+ sys .path .insert (0 , parent_dir )
9+
110import unittest
211from unittest import mock
312
817@mock .patch ('generate_user_credentials.Flow' )
918class MockFlow :
1019 @staticmethod
11- def from_client_secrets_file (client_secrets_file , scopes , redirect_uri ):
20+ # Production code calls Flow.from_client_secrets_file(client_secrets_path, scopes=scopes)
21+ # then sets flow.redirect_uri separately.
22+ def from_client_secrets_file (client_secrets_file , scopes ):
1223 mock_flow_instance = mock .Mock ()
24+ # Ensure redirect_uri is set on the instance if it's expected by other parts of the test/code.
25+ # The production code sets this explicitly after creating the flow object.
26+ # For the mock, we can simulate this if needed, or ensure assertions on 'flow.redirect_uri = ...' are made.
27+ # mock_flow_instance.redirect_uri = 'http://localhost:8080/' # Default or from _REDIRECT_URI
1328 mock_flow_instance .authorization_url .return_value = ('https://accounts.google.com/o/oauth2/auth?scope=test_scope&redirect_uri=http://localhost:8080/&response_type=code&client_id=test_client_id' , 'test_state' )
1429 mock_flow_instance .fetch_token .return_value = {'access_token' : 'test_access_token' }
1530 return mock_flow_instance
@@ -76,8 +91,8 @@ def test_parse_raw_query_params_no_query_parameters_path_only(self):
7691
7792 def test_parse_raw_query_params_no_query_parameters_empty_after_qmark (self ):
7893 raw_request = b"GET /? HTTP/1.1\r \n Host: localhost\r \n \r \n "
79- # query_string becomes "", pairs becomes [['']], params becomes {'': ''}
80- expected = {'' : '' }
94+ # For "GET /? HTTP...", params_str becomes "", so an empty dict is expected.
95+ expected = {}
8196 self .assertEqual (generate_user_credentials .parse_raw_query_params (raw_request ), expected )
8297
8398 def test_parse_raw_query_params_url_encoded_chars (self ):
@@ -102,80 +117,79 @@ def test_parse_raw_query_params_malformed_request_no_space(self):
102117 @mock .patch ('socket.socket' )
103118 def test_get_authorization_code_success (self , mock_socket_class , mock_parse_params , mock_sys_exit , mock_print ):
104119 # Configure socket mock
105- mock_socket_instance = mock . Mock ()
120+ mock_socket_instance = mock_socket_class . return_value # This is the 'sock' object
106121 mock_conn = mock .Mock ()
107122 mock_socket_instance .accept .return_value = (mock_conn , ('127.0.0.1' , 12345 ))
108- mock_socket_class .return_value .__enter__ .return_value = mock_socket_instance
123+ # mock_socket_class.return_value.__enter__.return_value = mock_socket_instance # Not using 'with' statement
109124
110125 # Configure parse_raw_query_params mock
111126 mock_parse_params .return_value = {'code' : 'test_auth_code' , 'state' : 'matching_state_token' }
112127
113128 auth_code = generate_user_credentials .get_authorization_code ('matching_state_token' )
114129
115130 self .assertEqual (auth_code , 'test_auth_code' )
116- mock_socket_instance .bind .assert_called_once_with (('localhost ' , 8080 ))
131+ mock_socket_instance .bind .assert_called_once_with (('127.0.0.1 ' , 8080 ))
117132 mock_socket_instance .listen .assert_called_once_with (1 )
118133 mock_socket_instance .accept .assert_called_once ()
119134 mock_conn .recv .assert_called_once ()
120135 mock_conn .sendall .assert_called_once () # Check that a response is sent
121136 # The exact content of sendall can be checked if necessary, e.g. mock_conn.sendall.assert_called_with(b"HTTP/1.1 200 OK\r\n...")
122137 mock_conn .close .assert_called_once ()
123138 mock_sys_exit .assert_not_called ()
124- mock_print . assert_any_call ( "Authorization code received: test_auth_code" )
125-
139+ # The success message is not printed to console in the actual code, only if an error occurs.
140+ # So, no mock_print assertion here for success.
126141
127142 @mock .patch ('builtins.print' )
128143 @mock .patch ('sys.exit' )
129144 @mock .patch ('generate_user_credentials.parse_raw_query_params' )
130145 @mock .patch ('socket.socket' )
131146 def test_get_authorization_code_no_code_parameter (self , mock_socket_class , mock_parse_params , mock_sys_exit , mock_print ):
132147 # Configure socket mock
133- mock_socket_instance = mock . Mock ()
148+ mock_socket_instance = mock_socket_class . return_value # This is the 'sock' object
134149 mock_conn = mock .Mock ()
135150 mock_socket_instance .accept .return_value = (mock_conn , ('127.0.0.1' , 12345 ))
136- mock_socket_class .return_value .__enter__ .return_value = mock_socket_instance
151+ # mock_socket_class.return_value.__enter__.return_value = mock_socket_instance # Not using 'with' statement
137152
138153 # Configure parse_raw_query_params mock
139154 mock_parse_params .return_value = {'error' : 'some_error' , 'state' : 'matching_state_token' }
140155
141156 generate_user_credentials .get_authorization_code ('matching_state_token' )
142157
143- mock_socket_instance .bind .assert_called_once_with (('localhost ' , 8080 ))
158+ mock_socket_instance .bind .assert_called_once_with (('127.0.0.1 ' , 8080 ))
144159 mock_socket_instance .listen .assert_called_once_with (1 )
145160 mock_socket_instance .accept .assert_called_once ()
146161 mock_conn .recv .assert_called_once ()
147162 # Check that a response is sent even in error cases before exit
148163 mock_conn .sendall .assert_called_once ()
149164 mock_conn .close .assert_called_once ()
150165 mock_sys_exit .assert_called_once_with (1 )
151- mock_print .assert_any_call ("Error: Could not retrieve authorization code. Query parameters: {'error': 'some_error', 'state': 'matching_state_token'}" )
152-
166+ # Print check removed, relying on sys.exit(1) to confirm error path
153167
154168 @mock .patch ('builtins.print' )
155169 @mock .patch ('sys.exit' )
156170 @mock .patch ('generate_user_credentials.parse_raw_query_params' )
157171 @mock .patch ('socket.socket' )
158172 def test_get_authorization_code_state_mismatch (self , mock_socket_class , mock_parse_params , mock_sys_exit , mock_print ):
159173 # Configure socket mock
160- mock_socket_instance = mock . Mock ()
174+ mock_socket_instance = mock_socket_class . return_value # This is the 'sock' object
161175 mock_conn = mock .Mock ()
162176 mock_socket_instance .accept .return_value = (mock_conn , ('127.0.0.1' , 12345 ))
163- mock_socket_class .return_value .__enter__ .return_value = mock_socket_instance
177+ # mock_socket_class.return_value.__enter__.return_value = mock_socket_instance # Not using 'with' statement
164178
165179 # Configure parse_raw_query_params mock
166180 mock_parse_params .return_value = {'code' : 'test_auth_code' , 'state' : 'mismatched_state_token' }
167181
168182 generate_user_credentials .get_authorization_code ('correct_state_token' )
169183
170- mock_socket_instance .bind .assert_called_once_with (('localhost ' , 8080 ))
184+ mock_socket_instance .bind .assert_called_once_with (('127.0.0.1 ' , 8080 ))
171185 mock_socket_instance .listen .assert_called_once_with (1 )
172186 mock_socket_instance .accept .assert_called_once ()
173187 mock_conn .recv .assert_called_once ()
174188 # Check that a response is sent even in error cases before exit
175189 mock_conn .sendall .assert_called_once ()
176190 mock_conn .close .assert_called_once ()
177191 mock_sys_exit .assert_called_once_with (1 )
178- mock_print . assert_any_call ( "Error: State token mismatch. Expected 'correct_state_token' but received 'mismatched_state_token'." )
192+ # Print check removed, relying on sys.exit(1) to confirm error path
179193
180194 @mock .patch ('builtins.print' )
181195 @mock .patch ('generate_user_credentials.get_authorization_code' )
@@ -193,39 +207,45 @@ def test_main_default_scope(self, mock_os_urandom, mock_hashlib_sha256, MockFlow
193207 mock_get_auth_code .return_value = 'dummy_auth_code_default'
194208
195209 mock_flow_instance = mock .Mock ()
196- mock_flow_instance .authorization_url .return_value = 'dummy_auth_url_default'
210+ # authorization_url should return a tuple: (url, state)
211+ mock_flow_instance .authorization_url .return_value = ('dummy_auth_url_default' , 'predictable_state_default' )
197212 # Simulate credentials object structure
198213 mock_credentials = mock .Mock ()
199214 mock_credentials .refresh_token = 'test_refresh_token_default'
200215 mock_flow_instance .credentials = mock_credentials
201216 MockFlowClass .from_client_secrets_file .return_value = mock_flow_instance
202217
203218 # Call the main function
204- default_scopes = ['https://www.googleapis.com/auth/adwords ' ]
219+ default_scopes = ['https://www.googleapis.com/auth/googleads ' ]
205220 generate_user_credentials .main ('client_secrets_default.json' , default_scopes )
206221
207222 # Assertions
208- mock_os_urandom .assert_called_once_with (32 )
223+ mock_os_urandom .assert_called_once_with (1024 )
209224 mock_hashlib_sha256 .assert_called_once_with (b'test_urandom_bytes_default' )
210225 mock_sha256_instance .hexdigest .assert_called_once ()
211226
212227 MockFlowClass .from_client_secrets_file .assert_called_once_with (
213228 'client_secrets_default.json' ,
214- scopes = default_scopes ,
215- redirect_uri = 'http://localhost:8080/'
229+ scopes = default_scopes
230+ # redirect_uri is not passed to from_client_secrets_file in production
216231 )
232+ # We can also assert that flow.redirect_uri was set correctly after the call
233+ # mock_flow_instance.redirect_uri = generate_user_credentials._REDIRECT_URI (or the specific mock instance)
234+ # self.assertEqual(mock_flow_instance.redirect_uri, generate_user_credentials._REDIRECT_URI)
235+
236+ # The state used in authorization_url call is the one generated by hashlib.sha256
217237 mock_flow_instance .authorization_url .assert_called_once_with (
218- state = 'predictable_state_default' ,
219- access_type = "offline" ,
220- prompt = "consent"
238+ access_type = "offline" , state = 'predictable_state_default' , prompt = "consent" , include_granted_scopes = "true"
221239 )
222- mock_print .assert_any_call ("Visit the following URL to authorize your application: dummy_auth_url_default" )
240+ mock_print .assert_any_call ("Paste this URL into your browser: " )
241+ mock_print .assert_any_call ('dummy_auth_url_default' )
223242 mock_get_auth_code .assert_called_once_with ('predictable_state_default' )
224243 mock_flow_instance .fetch_token .assert_called_once_with (code = 'dummy_auth_code_default' )
225- mock_print .assert_any_call ("Your refresh token is: test_refresh_token_default" )
244+ mock_print .assert_any_call ("\n Your refresh token is: test_refresh_token_default\n " )
226245 mock_print .assert_any_call (
227- "Add this refresh token to your google-ads.yaml file: \n "
228- "refresh_token: test_refresh_token_default"
246+ "Add your refresh token to your client library configuration as "
247+ "described here: "
248+ "https://developers.google.com/google-ads/api/docs/client-libs/python/configuration"
229249 )
230250
231251 @mock .patch ('builtins.print' )
@@ -244,38 +264,40 @@ def test_main_with_additional_scopes(self, mock_os_urandom, mock_hashlib_sha256,
244264 mock_get_auth_code .return_value = 'dummy_auth_code_additional'
245265
246266 mock_flow_instance = mock .Mock ()
247- mock_flow_instance .authorization_url .return_value = 'dummy_auth_url_additional'
267+ # authorization_url should return a tuple: (url, state)
268+ mock_flow_instance .authorization_url .return_value = ('dummy_auth_url_additional' , 'predictable_state_additional' )
248269 mock_credentials = mock .Mock ()
249270 mock_credentials .refresh_token = 'test_refresh_token_additional'
250271 mock_flow_instance .credentials = mock_credentials
251272 MockFlowClass .from_client_secrets_file .return_value = mock_flow_instance
252273
253274 # Call the main function
254- additional_scopes = ['https://www.googleapis.com/auth/adwords ' , 'scope1' , 'scope2' ]
275+ additional_scopes = ['https://www.googleapis.com/auth/googleads ' , 'scope1' , 'scope2' ]
255276 generate_user_credentials .main ('client_secrets_additional.json' , additional_scopes )
256277
257278 # Assertions
258- mock_os_urandom .assert_called_once_with (32 )
279+ mock_os_urandom .assert_called_once_with (1024 )
259280 mock_hashlib_sha256 .assert_called_once_with (b'test_urandom_bytes_additional' )
260281 mock_sha256_instance .hexdigest .assert_called_once ()
261282
262283 MockFlowClass .from_client_secrets_file .assert_called_once_with (
263284 'client_secrets_additional.json' ,
264- scopes = additional_scopes ,
265- redirect_uri = 'http://localhost:8080/'
285+ scopes = additional_scopes
286+ # redirect_uri is not passed to from_client_secrets_file in production
266287 )
288+ # The state used in authorization_url call is the one generated by hashlib.sha256
267289 mock_flow_instance .authorization_url .assert_called_once_with (
268- state = 'predictable_state_additional' ,
269- access_type = "offline" ,
270- prompt = "consent"
290+ access_type = "offline" , state = 'predictable_state_additional' , prompt = "consent" , include_granted_scopes = "true"
271291 )
272- mock_print .assert_any_call ("Visit the following URL to authorize your application: dummy_auth_url_additional" )
292+ mock_print .assert_any_call ("Paste this URL into your browser: " )
293+ mock_print .assert_any_call ('dummy_auth_url_additional' )
273294 mock_get_auth_code .assert_called_once_with ('predictable_state_additional' )
274295 mock_flow_instance .fetch_token .assert_called_once_with (code = 'dummy_auth_code_additional' )
275- mock_print .assert_any_call ("Your refresh token is: test_refresh_token_additional" )
296+ mock_print .assert_any_call ("\n Your refresh token is: test_refresh_token_additional\n " )
276297 mock_print .assert_any_call (
277- "Add this refresh token to your google-ads.yaml file: \n "
278- "refresh_token: test_refresh_token_additional"
298+ "Add your refresh token to your client library configuration as "
299+ "described here: "
300+ "https://developers.google.com/google-ads/api/docs/client-libs/python/configuration"
279301 )
280302
281303if __name__ == '__main__' :
0 commit comments