"""Tests for database logic.""" from botocore.stub import Stubber import pytest from src.connectors.boto_clients import rds_client from src.logic.database import _poll_for_pending_modifications from src.logic.database import DatabaseNotFound from src.logic.database import find_database_identifier from src.logic.database import reset_rds_master_credentials from src.logic.database import UnsupportedDatabaseType @pytest.fixture def rds_stubber(): """Return stubber for the RDS client.""" with Stubber(rds_client) as stubber: yield stubber def test_find_database_identifier_cluster(rds_stubber): """Test finding database identifier for a cluster.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db-0' } ] }, { 'Filters': [ { 'Name': 'db-cluster-id', 'Values': ['my-db'] } ] }) identifier, db_type = find_database_identifier('my-db') assert identifier == 'my-db' assert db_type == 'cluster' rds_stubber.assert_no_pending_responses() def test_find_database_identifier_standalone(rds_stubber): """Test finding database identifier for a standalone instance.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }, { 'Filters': [ { 'Name': 'db-cluster-id', 'Values': ['my-db'] } ] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db' } ] }, { 'Filters': [ { 'Name': 'db-instance-id', 'Values': ['my-db'] } ] }) identifier, db_type = find_database_identifier('my-db') assert identifier == 'my-db' assert db_type == 'standalone' rds_stubber.assert_no_pending_responses() def test_find_database_identifier_not_found(rds_stubber): """Test an exception is thrown if database not found.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }, { 'Filters': [ { 'Name': 'db-cluster-id', 'Values': ['my-db'] } ] }) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [] }, { 'Filters': [ { 'Name': 'db-instance-id', 'Values': ['my-db'] } ] }) with pytest.raises(DatabaseNotFound): find_database_identifier('my-db') def test_reset_rds_master_credentials_cluster(rds_stubber, mocker): """Test resetting of master credentials for a cluster.""" mocker.patch( 'src.utils.password.generate_random_password', return_value='my-password') rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'Endpoint': 'my-db-endpoint', 'MasterUsername': 'my-db-username' } ] }) _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'ApplyImmediately': True, 'MasterUserPassword': 'my-password' }) _mock_cluster_available(rds_stubber, 'my-db') connection_credentials = reset_rds_master_credentials( 'my-db', 'cluster', wait=True) assert connection_credentials == { 'host': 'my-db-endpoint', 'username': 'my-db-username', 'password': 'my-password' } rds_stubber.assert_no_pending_responses() def test_reset_rds_master_credentials_standalone(rds_stubber, mocker): """Test resetting of master credentials for a standalone db.""" mocker.patch( 'src.utils.password.generate_random_password', return_value='my-password') rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'Endpoint': { 'Address': 'my-db-endpoint' }, 'MasterUsername': 'my-db-username' } ] }) _mock_instance_available(rds_stubber, 'my-db') rds_stubber.add_response('modify_db_instance', {}, { 'DBInstanceIdentifier': 'my-db', 'ApplyImmediately': True, 'MasterUserPassword': 'my-password' }) _mock_instance_available(rds_stubber, 'my-db') connection_credentials = reset_rds_master_credentials( 'my-db', 'standalone', wait=True) assert connection_credentials == { 'host': 'my-db-endpoint', 'username': 'my-db-username', 'password': 'my-password' } rds_stubber.assert_no_pending_responses() def test_reset_rds_master_credentials_cluster_retries_on_invalid_state( rds_stubber, mocker): """Test that InvalidDBClusterStateFault triggers a retry and succeeds.""" mocker.patch( 'src.utils.password.generate_random_password', return_value='my-password') mocker.patch('config.PASSWORD_RESET_RETRY_DELAY', 0) rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'Endpoint': 'my-db-endpoint', 'MasterUsername': 'my-db-username' } ] }) _mock_cluster_available(rds_stubber, 'my-db') rds_stubber.add_client_error( 'modify_db_cluster', service_error_code='InvalidDBClusterStateFault') rds_stubber.add_response('modify_db_cluster', {}, { 'DBClusterIdentifier': 'my-db', 'ApplyImmediately': True, 'MasterUserPassword': 'my-password' }) _mock_cluster_available(rds_stubber, 'my-db') connection_credentials = reset_rds_master_credentials( 'my-db', 'cluster', wait=True) assert connection_credentials == { 'host': 'my-db-endpoint', 'username': 'my-db-username', 'password': 'my-password' } rds_stubber.assert_no_pending_responses() def test_reset_rds_master_credentials_standalone_retries_on_invalid_state( rds_stubber, mocker): """Test that InvalidDBInstanceStateFault triggers a retry and succeeds.""" mocker.patch( 'src.utils.password.generate_random_password', return_value='my-password') mocker.patch('config.PASSWORD_RESET_RETRY_DELAY', 0) rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'Endpoint': { 'Address': 'my-db-endpoint' }, 'MasterUsername': 'my-db-username' } ] }) _mock_instance_available(rds_stubber, 'my-db') _mock_instance_available(rds_stubber, 'my-db') # The botocore Stubber doesn't produce the specific exception subclass # for InvalidDBInstanceStateFault, so inject it directly via patch. exc = rds_client.exceptions.InvalidDBInstanceStateFault( {'Error': {'Code': 'InvalidDBInstanceStateFault', 'Message': ''}}, 'ModifyDBInstance', ) mocker.patch.object( rds_client, 'modify_db_instance', side_effect=[exc, {}]) connection_credentials = reset_rds_master_credentials( 'my-db', 'standalone', wait=True) assert connection_credentials == { 'host': 'my-db-endpoint', 'username': 'my-db-username', 'password': 'my-password' } rds_stubber.assert_no_pending_responses() def test_reset_rds_master_credentials_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): reset_rds_master_credentials('my-db', 'unsupported', wait=True) def test_poll_for_pending_modifications_cluster_pending_changes(rds_stubber): """Test polling where there are pending modifications to a cluster.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': 'my-db', 'PendingModifiedValues': { 'IAMDatabaseAuthenticationEnabled': True } } ] }) assert _poll_for_pending_modifications('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_cluster_no_pending_changes( rds_stubber): """Test polling where there are no pending modifications to a cluster.""" rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': 'my-db' } ] }) assert not _poll_for_pending_modifications('my-db', 'cluster') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_standalone_pending_changes( rds_stubber): """Test poll where there are pending changes to a standalone db.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db', 'PendingModifiedValues': { 'IAMDatabaseAuthenticationEnabled': True } } ] }) assert _poll_for_pending_modifications('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_standalone_no_pending_changes( rds_stubber): """Test poll where there are no pending changes to a standalone db.""" rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': 'my-db', 'PendingModifiedValues': {} } ] }) assert not _poll_for_pending_modifications('my-db', 'standalone') rds_stubber.assert_no_pending_responses() def test_poll_for_pending_modifications_unsupported_type(): """Test exception is thrown for an unsupported database type.""" with pytest.raises(UnsupportedDatabaseType): _poll_for_pending_modifications('my-db', 'unsupported') def _mock_cluster_available(rds_stubber, cluster_id): rds_stubber.add_response('describe_db_clusters', { 'DBClusters': [ { 'DBClusterIdentifier': cluster_id, 'Status': 'available' } ] }) def _mock_instance_available(rds_stubber, instance_id): rds_stubber.add_response('describe_db_instances', { 'DBInstances': [ { 'DBInstanceIdentifier': instance_id, 'DBInstanceStatus': 'available', 'PendingModifiedValues': {} } ] })