1+ import datetime
12import typing
23from test .unit import MockCredentialsProvider
4+ from unittest import mock
5+ from unittest .mock import MagicMock , call
36
47import pytest # type: ignore
8+ from dateutil .tz import tzutc
59from pytest_mock import mocker
610
7- from redshift_connector import InterfaceError , RedshiftProperty , set_iam_properties
11+ from redshift_connector import InterfaceError , RedshiftProperty
812from redshift_connector .auth import AWSCredentialsProvider
913from redshift_connector .config import ClientProtocolVersion
10- from redshift_connector .iam_helper import set_iam_credentials
14+ from redshift_connector .iam_helper import IamHelper
1115from redshift_connector .plugin import (
1216 AdfsCredentialsProvider ,
1317 AzureCredentialsProvider ,
2024
2125@pytest .fixture
2226def mock_set_iam_credentials (mocker ):
23- mocker .patch ("redshift_connector.iam_helper.set_iam_credentials" , return_value = None )
27+ mocker .patch ("redshift_connector.iam_helper.IamHelper. set_iam_credentials" , return_value = None )
2428
2529
2630@pytest .fixture
2731def mock_set_cluster_credentials (mocker ):
28- mocker .patch ("redshift_connector.iam_helper.set_cluster_credentials" , return_value = None )
32+ mocker .patch ("redshift_connector.iam_helper.IamHelper. set_cluster_credentials" , return_value = None )
2933
3034
3135@pytest .fixture
@@ -99,7 +103,7 @@ def get_set_iam_properties_args(**kwargs) -> typing.Dict[str, typing.Any]:
99103def test_set_iam_properties_fails_when_info_is_none (missing_param ):
100104 keywords : typing .Dict = {missing_param : None }
101105 with pytest .raises (InterfaceError ) as excinfo :
102- set_iam_properties (** get_set_iam_properties_args (** keywords ))
106+ IamHelper . set_iam_properties (** get_set_iam_properties_args (** keywords ))
103107 assert "Invalid connection property setting" in str (excinfo .value )
104108
105109
@@ -123,7 +127,7 @@ def test_set_iam_properties_enforce_min_ssl_mode(ssl_param):
123127 all_params : typing .Dict = get_set_iam_properties_args (** keywords )
124128 assert all_params ["sslmode" ] == test_input
125129
126- set_iam_properties (** all_params )
130+ IamHelper . set_iam_properties (** all_params )
127131 assert all_params ["info" ].sslmode == expected_mode
128132
129133
@@ -136,7 +140,7 @@ def test_set_iam_properties_enforce_client_protocol_version(_input):
136140 all_params : typing .Dict = get_set_iam_properties_args (** keywords )
137141 assert all_params ["client_protocol_version" ] == _input
138142
139- set_iam_properties (** all_params )
143+ IamHelper . set_iam_properties (** all_params )
140144 assert all_params ["info" ].client_protocol_version == _input
141145
142146
@@ -223,7 +227,7 @@ def test_set_iam_properties_enforce_setting_compatibility(mocker, joint_params):
223227 test_input , expected_exception_msg = joint_params
224228
225229 with pytest .raises (InterfaceError ) as excinfo :
226- set_iam_properties (** get_set_iam_properties_args (** test_input ))
230+ IamHelper . set_iam_properties (** get_set_iam_properties_args (** test_input ))
227231 assert expected_exception_msg in str (excinfo .value )
228232
229233
@@ -241,6 +245,8 @@ def make_redshift_property() -> RedshiftProperty:
241245 rp : RedshiftProperty = RedshiftProperty ()
242246 rp .user_name = "mario@luigi.com"
243247 rp .password = "bowser"
248+ rp .db_name = "dev"
249+ rp .cluster_identifier = "something"
244250 rp .idp_host = "8000"
245251 rp .duration = 100
246252 rp .preferred_role = "analyst"
@@ -270,7 +276,7 @@ def test_set_iam_properties_provider_assigned(mocker, provider):
270276
271277 spy = mocker .spy (expectedProvider , "add_parameter" )
272278
273- set_iam_credentials (rp )
279+ IamHelper . set_iam_credentials (rp )
274280 assert spy .called
275281 assert spy .call_count == 1
276282 # ensure call to add_Parameter was made on the expected Provider class
@@ -293,8 +299,8 @@ def test_set_iam_properties_via_aws_credentials(mocker, test_input):
293299 info_obj ["iam" ] = True
294300 info_obj ["cluster_identifier" ] = "blah"
295301
296- mocker .patch ("redshift_connector.iam_helper.set_iam_credentials" , return_value = None )
297- set_iam_properties (** info_obj )
302+ mocker .patch ("redshift_connector.iam_helper.IamHelper. set_iam_credentials" , return_value = None )
303+ IamHelper . set_iam_properties (** info_obj )
298304
299305 for aws_cred_key , aws_cred_val in enumerate (test_input ):
300306 if aws_cred_key == "profile" :
@@ -316,10 +322,10 @@ def test_set_iam_credentials_via_aws_credentials(mocker):
316322 redshift_property .secret_access_key = "secret_val"
317323 redshift_property .session_token = "session_val"
318324
319- mocker .patch ("redshift_connector.iam_helper.set_cluster_credentials" , return_value = None )
325+ mocker .patch ("redshift_connector.iam_helper.IamHelper. set_cluster_credentials" , return_value = None )
320326 spy = mocker .spy (AWSCredentialsProvider , "add_parameter" )
321327
322- set_iam_credentials (redshift_property )
328+ IamHelper . set_iam_credentials (redshift_property )
323329 assert spy .called is True
324330 assert spy .call_count == 1
325331 assert spy .call_args [0 ][1 ] == redshift_property
@@ -328,14 +334,155 @@ def test_set_iam_credentials_via_aws_credentials(mocker):
328334def test_dynamically_loading_credential_holder (mocker ):
329335 external_class_name : str = "test.unit.MockCredentialsProvider"
330336 mocker .patch ("{}.get_credentials" .format (external_class_name ))
331- mocker .patch ("redshift_connector.iam_helper.set_cluster_credentials" , return_value = None )
337+ mocker .patch ("redshift_connector.iam_helper.IamHelper. set_cluster_credentials" , return_value = None )
332338 rp : RedshiftProperty = make_redshift_property ()
333339 rp .credentials_provider = external_class_name
334340
335341 spy = mocker .spy (MockCredentialsProvider , "add_parameter" )
336342
337- set_iam_credentials (rp )
343+ IamHelper . set_iam_credentials (rp )
338344 assert spy .called
339345 assert spy .call_count == 1
340346 # ensure call to add_Parameter was made on the expected Provider class
341347 assert isinstance (spy .call_args [0 ][0 ], MockCredentialsProvider ) is True
348+
349+
350+ def test_get_credentials_cache_key ():
351+ rp : RedshiftProperty = RedshiftProperty ()
352+ rp .db_user = "2"
353+ rp .db_name = "1"
354+ rp .db_groups = ["4" , "3" , "5" ]
355+ rp .cluster_identifier = "6"
356+ rp .auto_create = False
357+
358+ res_cache_key : str = IamHelper .get_credentials_cache_key (rp )
359+ assert res_cache_key is not None
360+ assert res_cache_key == "2;1;3,4,5;6;False"
361+
362+
363+ def test_get_credentials_cache_key_no_db_groups ():
364+ rp : RedshiftProperty = RedshiftProperty ()
365+ rp .db_user = "2"
366+ rp .db_name = "1"
367+ rp .cluster_identifier = "6"
368+ rp .auto_create = False
369+
370+ res_cache_key : str = IamHelper .get_credentials_cache_key (rp )
371+ assert res_cache_key is not None
372+ assert res_cache_key == "2;1;;6;False"
373+
374+
375+ @mock .patch ("boto3.client.get_cluster_credentials" )
376+ @mock .patch ("boto3.client.describe_clusters" )
377+ @mock .patch ("boto3.client" )
378+ def test_set_cluster_credentials_caches_credentials (
379+ mock_boto_client , mock_describe_clusters , mock_get_cluster_credentials
380+ ):
381+ mock_cred_provider = MagicMock ()
382+ mock_cred_holder = MagicMock ()
383+ mock_cred_provider .get_credentials .return_value = mock_cred_holder
384+ mock_cred_holder .has_associated_session = False
385+
386+ rp : RedshiftProperty = make_redshift_property ()
387+
388+ IamHelper .credentials_cache .clear ()
389+
390+ IamHelper .set_cluster_credentials (mock_cred_provider , rp )
391+ assert len (IamHelper .credentials_cache ) == 1
392+
393+ assert mock_boto_client .called is True
394+ mock_boto_client .assert_has_calls (
395+ [
396+ call ().get_cluster_credentials (
397+ AutoCreate = rp .auto_create ,
398+ ClusterIdentifier = rp .cluster_identifier ,
399+ DbGroups = rp .db_groups ,
400+ DbName = rp .db_name ,
401+ DbUser = rp .db_user ,
402+ )
403+ ]
404+ )
405+
406+
407+ @mock .patch ("boto3.client.get_cluster_credentials" )
408+ @mock .patch ("boto3.client.describe_clusters" )
409+ @mock .patch ("boto3.client" )
410+ def test_set_cluster_credentials_uses_cache_if_possible (
411+ mock_boto_client , mock_describe_clusters , mock_get_cluster_credentials
412+ ):
413+ mock_cred_provider = MagicMock ()
414+ mock_cred_holder = MagicMock ()
415+ mock_cred_provider .get_credentials .return_value = mock_cred_holder
416+ mock_cred_holder .has_associated_session = False
417+
418+ rp : RedshiftProperty = make_redshift_property ()
419+ # mock out the boto3 response temporary credentials stored from prior auth
420+ mock_cred_obj : typing .Dict [str , typing .Union [str , datetime .datetime ]] = {
421+ "DbUser" : "xyz" ,
422+ "DbPassword" : "turtle" ,
423+ "Expiration" : datetime .datetime (9999 , 1 , 1 , tzinfo = tzutc ()),
424+ }
425+ # populate the cache
426+ IamHelper .credentials_cache .clear ()
427+ IamHelper .credentials_cache [IamHelper .get_credentials_cache_key (rp )] = mock_cred_obj
428+
429+ IamHelper .set_cluster_credentials (mock_cred_provider , rp )
430+ assert len (IamHelper .credentials_cache ) == 1
431+ assert IamHelper .credentials_cache [IamHelper .get_credentials_cache_key (rp )] is mock_cred_obj
432+ assert mock_boto_client .called is True
433+
434+ assert rp .user_name == mock_cred_obj ["DbUser" ]
435+ assert rp .password == mock_cred_obj ["DbPassword" ]
436+
437+ assert (
438+ call ().get_cluster_credentials (
439+ AutoCreate = rp .auto_create ,
440+ ClusterIdentifier = rp .cluster_identifier ,
441+ DbGroups = rp .db_groups ,
442+ DbName = rp .db_name ,
443+ DbUser = rp .db_user ,
444+ )
445+ not in mock_boto_client .mock_calls
446+ )
447+
448+
449+ @mock .patch ("boto3.client.get_cluster_credentials" )
450+ @mock .patch ("boto3.client.describe_clusters" )
451+ @mock .patch ("boto3.client" )
452+ def test_set_cluster_credentials_refreshes_stale_credentials (
453+ mock_boto_client , mock_describe_clusters , mock_get_cluster_credentials
454+ ):
455+ mock_cred_provider = MagicMock ()
456+ mock_cred_holder = MagicMock ()
457+ mock_cred_provider .get_credentials .return_value = mock_cred_holder
458+ mock_cred_holder .has_associated_session = False
459+
460+ rp : RedshiftProperty = make_redshift_property ()
461+ # mock out the boto3 response temporary credentials stored from prior auth (now stale)
462+ mock_cred_obj : typing .Dict [str , typing .Union [str , datetime .datetime ]] = {
463+ "DbUser" : "xyz" ,
464+ "DbPassword" : "turtle" ,
465+ "Expiration" : datetime .datetime (1 , 1 , 1 , tzinfo = tzutc ()),
466+ }
467+ # populate the cache
468+ IamHelper .credentials_cache .clear ()
469+ IamHelper .credentials_cache [IamHelper .get_credentials_cache_key (rp )] = mock_cred_obj
470+
471+ IamHelper .set_cluster_credentials (mock_cred_provider , rp )
472+ assert len (IamHelper .credentials_cache ) == 1
473+ # ensure new temporary credentials have been replaced in cache
474+ assert IamHelper .get_credentials_cache_key (rp ) in IamHelper .credentials_cache
475+ assert IamHelper .credentials_cache [IamHelper .get_credentials_cache_key (rp )] is not mock_cred_obj
476+ assert mock_boto_client .called is True
477+
478+ mock_boto_client .assert_has_calls (
479+ [
480+ call ().get_cluster_credentials (
481+ AutoCreate = rp .auto_create ,
482+ ClusterIdentifier = rp .cluster_identifier ,
483+ DbGroups = rp .db_groups ,
484+ DbName = rp .db_name ,
485+ DbUser = rp .db_user ,
486+ )
487+ ]
488+ )
0 commit comments