@@ -85,137 +85,138 @@ async def test_acquiring_cancellation():
8585 t .acquire_access_token .return_value = None
8686 await t .get ()
8787
88+ @pytest .mark .asyncio
89+ async def test_external_account_as_io ():
90+ service_data = {
91+ 'type' : 'external_account' ,
92+ 'audience' : (
93+ '//iam.googleapis.com/projects/123456/locations/global'
94+ '/workloadIdentityPools/pool/subject'
95+ ),
96+ 'subject_token_type' : 'urn:ietf:params:oauth:token-type:jwt' ,
97+ 'token_url' : 'https://sts.googleapis.com/v1/token' ,
98+ 'credential_source' : {
99+ 'type' : 'url' ,
100+ 'url' : 'http://169.254.169.254/metadata/identity/oauth2/token' ,
101+ 'headers' : {'Metadata' : 'true' },
102+ },
103+ }
104+
105+ service_file = io .StringIO (json .dumps (service_data ))
106+ t = token .BaseToken (service_file = service_file )
107+
108+ assert t .token_type == token .Type .EXTERNAL_ACCOUNT
109+ assert t .token_uri == 'https://oauth2.googleapis.com/token'
88110
89- @pytest .mark .asyncio
90- async def test_external_account_as_io ():
91- service_data = {
92- 'type' : 'external_account' ,
93- 'audience' : (
94- '//iam.googleapis.com/projects/123456/locations/global'
95- '/workloadIdentityPools/pool/subject'
96- ),
97- 'subject_token_type' : 'urn:ietf:params:oauth:token-type:jwt' ,
98- 'token_url' : 'https://sts.googleapis.com/v1/token' ,
99- 'credential_source' : {
100- 'type' : 'url' ,
101- 'url' : 'http://169.254.169.254/metadata/identity/oauth2/token' ,
102- 'headers' : {'Metadata' : 'true' },
103- },
104- }
111+ @pytest .mark .asyncio
112+ async def test_external_account_missing_required_fields ():
113+ # Missing token_url and credential_source
114+ service_data = {
115+ 'type' : 'external_account' ,
116+ 'audience' : (
117+ '//iam.googleapis.com/projects/123456/locations/global'
118+ '/workloadIdentityPools/pool/subject'
119+ ),
120+ 'subject_token_type' : 'urn:ietf:params:oauth:token-type:jwt' ,
121+ }
122+
123+ service_file = io .StringIO (json .dumps (service_data ))
124+ with mock .patch (
125+ 'gcloud.aio.auth.token.get_service_data' , return_value = service_data
126+ ):
127+ with pytest .raises (
128+ ValueError ,
129+ match = 'external_account credentials missing required fields' ,
130+ ):
131+ await token .Token (service_file = service_file ).get ()
105132
106- service_file = io .StringIO (json .dumps (service_data ))
107- t = token .BaseToken (service_file = service_file )
133+ @pytest .mark .asyncio
134+ async def test_external_account_token_refresh ():
135+ service_data = {
136+ 'type' : 'external_account' ,
137+ 'audience' : (
138+ '//iam.googleapis.com/projects/123456/locations/global'
139+ '/workloadIdentityPools/pool/subject'
140+ ),
141+ 'subject_token_type' : 'urn:ietf:params:oauth:token-type:jwt' ,
142+ 'token_url' : 'https://sts.googleapis.com/v1/token' ,
143+ 'credential_source' : {
144+ 'type' : 'url' ,
145+ 'url' : 'http://169.254.169.254/metadata/identity/oauth2/token' ,
146+ 'headers' : {'Metadata' : 'true' },
147+ },
148+ }
149+
150+ service_file = io .StringIO (f'{ json .dumps (service_data )} ' )
151+ t = token .Token (service_file = service_file )
152+
153+ # Mock the session to return a subject token
154+ mock_response = mock .AsyncMock ()
155+ mock_response .status = 200
156+ mock_response .text = 'subject_token_123'
157+ t .session .get = mock .AsyncMock (return_value = mock_response )
158+
159+ # Mock the token exchange response
160+ mock_token_response = mock .AsyncMock ()
161+ mock_token_response .status = 200
162+ mock_token_response .json = mock .AsyncMock (
163+ return_value = {
164+ 'access_token' : 'access_token_123' ,
165+ 'expires_in' : 3600 ,
166+ },
167+ )
168+ t .session .post = mock .AsyncMock (return_value = mock_token_response )
169+
170+ # Test token refresh
171+ token_response = await t ._refresh_external_account (timeout = 10 )
172+ assert token_response .value == 'access_token_123'
173+ assert token_response .expires_in == 3600
174+
175+ # Verify the correct requests were made
176+ t .session .get .assert_called_once_with (
177+ 'http://169.254.169.254/metadata/identity/oauth2/token' ,
178+ headers = {'Metadata' : 'true' },
179+ timeout = 10 ,
180+ )
108181
109- assert t .token_type == token .Type .EXTERNAL_ACCOUNT
110- assert t .token_uri == 'https://oauth2.googleapis.com/token'
182+ t .session .post .assert_called_once ()
183+ call_args = t .session .post .call_args
184+ assert call_args [0 ][0 ] == 'https://sts.googleapis.com/v1/token'
111185
186+ @pytest .mark .asyncio
187+ async def test_external_account_credential_source_types ():
188+ # Test URL credential source
189+ url_source = {
190+ 'type' : 'url' ,
191+ 'url' : 'http://example.com/token' ,
192+ 'headers' : {'Authorization' : 'Bearer secret' },
193+ }
194+ t = token .Token ()
195+ mock_response = mock .AsyncMock ()
196+ mock_response .status = 200
197+ mock_response .text = 'token_from_url'
198+ t .session .get = mock .AsyncMock (return_value = mock_response )
199+ token_value = await t ._get_subject_token (url_source , timeout = 10 )
200+ assert token_value == 'token_from_url'
201+
202+ # Test file credential source
203+ file_source = {'type' : 'file' , 'file' : 'test_token.txt' }
204+ with mock .patch (
205+ 'builtins.open' ,
206+ mock .mock_open (read_data = 'token_from_file' ),
207+ ):
208+ token_value = await t ._get_subject_token (file_source , timeout = 10 )
209+ assert token_value == 'token_from_file'
112210
113- @pytest .mark .asyncio
114- async def test_external_account_missing_required_fields ():
115- # Missing token_url and credential_source
116- service_data = {
117- 'type' : 'external_account' ,
118- 'audience' : (
119- '//iam.googleapis.com/projects/123456/locations/global'
120- '/workloadIdentityPools/pool/subject'
121- ),
122- 'subject_token_type' : 'urn:ietf:params:oauth:token-type:jwt' ,
123- }
211+ # Test environment credential source
212+ env_source = {'type' : 'environment' , 'environment_id' : 'TEST_TOKEN' }
213+ with mock .patch .dict ('os.environ' , {'TEST_TOKEN' : 'token_from_env' }):
214+ token_value = await t ._get_subject_token (env_source , timeout = 10 )
215+ assert token_value == 'token_from_env'
124216
125- service_file = io .StringIO (json .dumps (service_data ))
126- with mock .patch (
127- 'gcloud.aio.auth.token.get_service_data' , return_value = service_data
128- ):
217+ # Test invalid credential source type
218+ invalid_source = {'type' : 'invalid' }
129219 with pytest .raises (
130- ValueError ,
131- match = 'external_account credentials missing required fields' ,
220+ ValueError , match = 'unsupported credential_source type' ,
132221 ):
133- await token .Token (service_file = service_file ).get ()
134-
135-
136- @pytest .mark .asyncio
137- async def test_external_account_token_refresh ():
138- service_data = {
139- 'type' : 'external_account' ,
140- 'audience' : (
141- '//iam.googleapis.com/projects/123456/locations/global'
142- '/workloadIdentityPools/pool/subject'
143- ),
144- 'subject_token_type' : 'urn:ietf:params:oauth:token-type:jwt' ,
145- 'token_url' : 'https://sts.googleapis.com/v1/token' ,
146- 'credential_source' : {
147- 'type' : 'url' ,
148- 'url' : 'http://169.254.169.254/metadata/identity/oauth2/token' ,
149- 'headers' : {'Metadata' : 'true' },
150- },
151- }
152-
153- service_file = io .StringIO (f'{ json .dumps (service_data )} ' )
154- t = token .Token (service_file = service_file )
155-
156- # Mock the session to return a subject token
157- mock_response = mock .AsyncMock ()
158- mock_response .status = 200
159- mock_response .text = 'subject_token_123'
160- t .session .get = mock .AsyncMock (return_value = mock_response )
161-
162- # Mock the token exchange response
163- mock_token_response = mock .AsyncMock ()
164- mock_token_response .status = 200
165- mock_token_response .json = mock .AsyncMock (
166- return_value = {'access_token' : 'access_token_123' , 'expires_in' : 3600 }
167- )
168- t .session .post = mock .AsyncMock (return_value = mock_token_response )
169-
170- # Test token refresh
171- token_response = await t ._refresh_external_account (timeout = 10 )
172- assert token_response .value == 'access_token_123'
173- assert token_response .expires_in == 3600
174-
175- # Verify the correct requests were made
176- t .session .get .assert_called_once_with (
177- 'http://169.254.169.254/metadata/identity/oauth2/token' ,
178- headers = {'Metadata' : 'true' },
179- timeout = 10 ,
180- )
181-
182- t .session .post .assert_called_once ()
183- call_args = t .session .post .call_args
184- assert call_args [0 ][0 ] == 'https://sts.googleapis.com/v1/token'
185-
186-
187- @pytest .mark .asyncio
188- async def test_external_account_credential_source_types ():
189- # Test URL credential source
190- url_source = {
191- 'type' : 'url' ,
192- 'url' : 'http://example.com/token' ,
193- 'headers' : {'Authorization' : 'Bearer secret' },
194- }
195- t = token .Token ()
196- mock_response = mock .AsyncMock ()
197- mock_response .status = 200
198- mock_response .text = 'token_from_url'
199- t .session .get = mock .AsyncMock (return_value = mock_response )
200- token_value = await t ._get_subject_token (url_source , timeout = 10 )
201- assert token_value == 'token_from_url'
202-
203- # Test file credential source
204- file_source = {'type' : 'file' , 'file' : 'test_token.txt' }
205- with mock .patch (
206- 'builtins.open' ,
207- mock .mock_open (read_data = 'token_from_file' ),
208- ):
209- token_value = await t ._get_subject_token (file_source , timeout = 10 )
210- assert token_value == 'token_from_file'
211-
212- # Test environment credential source
213- env_source = {'type' : 'environment' , 'environment_id' : 'TEST_TOKEN' }
214- with mock .patch .dict ('os.environ' , {'TEST_TOKEN' : 'token_from_env' }):
215- token_value = await t ._get_subject_token (env_source , timeout = 10 )
216- assert token_value == 'token_from_env'
217-
218- # Test invalid credential source type
219- invalid_source = {'type' : 'invalid' }
220- with pytest .raises (ValueError , match = 'unsupported credential_source type' ):
221- await t ._get_subject_token (invalid_source , timeout = 10 )
222+ await t ._get_subject_token (invalid_source , timeout = 10 )
0 commit comments