"""Lambda test module.""" from unittest import mock import boto3 import pytest from moto import mock_aws from tenacity import RetryError import app as index import config @pytest.fixture() def mock_account_id(): """Return an AWS account id.""" return '123456789000' @pytest.fixture() def mock_rds_cluster_name(): """Return an RDS cluster name.""" return 'prod-ows-cluster' @pytest.fixture() @mock_aws def mock_rds_client(mock_account_id, monkeypatch): """Return a mock RDS client.""" monkeypatch.setattr(index.config, 'EXTERNAL_ID', 'abc1234') credentials = index.assume_source_account_role(mock_account_id) client = boto3.client( 'rds', region_name=config.AWS_DEFAULT_REGION, aws_access_key_id=credentials['AccessKeyId'], aws_secret_access_key=credentials['SecretAccessKey'], aws_session_token=credentials['SessionToken'] ) return client @mock_aws def test_assume_source_account_role(mock_account_id, monkeypatch): """Test assume_source_account_role function.""" aws_credentials = ['AccessKeyId', 'SecretAccessKey', 'SessionToken'] monkeypatch.setattr(index.config, 'EXTERNAL_ID', 'abc1234') result = index.assume_source_account_role(mock_account_id) for credential in aws_credentials: assert result[credential] @mock_aws def test_get_target_account_id_default(): """Test get_target_account_id function when no account ID specified.""" result = index.get_target_account_id() assert result == '123456789012' @mock_aws def test_get_target_account_id_explicit(monkeypatch): """Test get_target_account_id function when account ID specified.""" monkeypatch.setattr('config.TARGET_ACCOUNT_ID', '234567890123') result = index.get_target_account_id() assert result == '234567890123' @mock_aws def test_create_initial_snapshot(mock_rds_client, mock_rds_cluster_name): """Test create_initial_snapshot function.""" mock_rds_client.create_db_cluster( AvailabilityZones=[ 'us-east-1a', ], BackupRetentionPeriod=1, DBClusterIdentifier=mock_rds_cluster_name, DatabaseName='dummy', Engine='aurora-mysql', EngineVersion='5.7.mysql_aurora.2.07.0', MasterUserPassword='dummypassword1234', MasterUsername='dummyuser', Port=3306, StorageEncrypted=True, Tags=[ { 'Key': 'environment', 'Value': 'prod' }, ] ) result = index.create_initial_snapshot( mock_rds_cluster_name, 'cluster', mock_rds_client) snapshots = mock_rds_client.describe_db_cluster_snapshots( DBClusterSnapshotIdentifier=result)['DBClusterSnapshots'] assert snapshots[0]['DBClusterSnapshotIdentifier'] == result @mock_aws def test_create_initial_snapshot_retry( mock_rds_client, mock_rds_cluster_name, mocker, monkeypatch): """Test create_initial_snapshot function with retry on errors.""" monkeypatch.setattr('config.SNAPSHOT_RETRY_LIMIT', 3) monkeypatch.setattr('config.SNAPSHOT_RETRY_DELAY', 0) mock_rds_client.create_db_cluster( AvailabilityZones=[ 'us-east-1a', ], BackupRetentionPeriod=1, DBClusterIdentifier=mock_rds_cluster_name, DatabaseName='dummy', Engine='aurora-mysql', EngineVersion='5.7.mysql_aurora.2.07.0', MasterUserPassword='dummypassword1234', MasterUsername='dummyuser', Port=3306, StorageEncrypted=True, Tags=[ { 'Key': 'environment', 'Value': 'prod' }, ] ) mocker.patch( 'common.logic.database.create_snapshot', side_effect=( mock_rds_client.exceptions.InvalidDBClusterStateFault( error_response={ 'Error': { 'Code': 'InvalidDBClusterStateFault' } }, operation_name='CreateDBClusterSnapshot' ), mock_rds_client.exceptions.InvalidDBClusterSnapshotStateFault( error_response={ 'Error': { 'Code': 'InvalidDBClusterSnapshotStateFault' } }, operation_name='CreateDBClusterSnapshot' ), f'{mock_rds_cluster_name}-snapshot' ) ) result = index.create_initial_snapshot( mock_rds_cluster_name, 'cluster', mock_rds_client) assert result == f'{mock_rds_cluster_name}-snapshot' @mock_aws def test_create_initial_snapshot_failure( mock_rds_client, mock_rds_cluster_name, mocker, monkeypatch): """Test create_initial_snapshot function with a failure.""" monkeypatch.setattr('config.SNAPSHOT_RETRY_LIMIT', 3) monkeypatch.setattr('config.SNAPSHOT_RETRY_DELAY', 0) mock_rds_client.create_db_cluster( AvailabilityZones=[ 'us-east-1a', ], BackupRetentionPeriod=1, DBClusterIdentifier=mock_rds_cluster_name, DatabaseName='dummy', Engine='aurora-mysql', EngineVersion='5.7.mysql_aurora.2.07.0', MasterUserPassword='dummypassword1234', MasterUsername='dummyuser', Port=3306, StorageEncrypted=True, Tags=[ { 'Key': 'environment', 'Value': 'prod' }, ] ) mocker.patch( 'common.logic.database.create_snapshot', side_effect=mock_rds_client.exceptions.InvalidDBClusterStateFault( error_response={'Error': {'Code': 'InvalidDBClusterStateFault'}}, operation_name='CreateDBClusterSnapshot' ) ) with pytest.raises(RetryError): index.create_initial_snapshot( mock_rds_cluster_name, 'cluster', mock_rds_client) @mock_aws def test_create_shareable_snapshot( mock_account_id, mock_rds_client, mock_rds_cluster_name): """Test create_shareable_snapshot function.""" mock_rds_client.create_db_cluster( AvailabilityZones=[ 'us-east-1a', ], BackupRetentionPeriod=1, DBClusterIdentifier=mock_rds_cluster_name, DatabaseName='dummy', Engine='aurora-mysql', EngineVersion='5.7.mysql_aurora.2.07.0', MasterUserPassword='dummypassword1234', MasterUsername='dummyuser', Port=3306, StorageEncrypted=True, Tags=[ { 'Key': 'environment', 'Value': 'prod' }, ] ) result = index.create_initial_snapshot( mock_rds_cluster_name, 'cluster', mock_rds_client) """ Moto has not implemented the copy_db_cluster_snapshot method, so mock its response here. However, the waiter in the database.copy_snapshot method needs an actual snapshot to exist, so create one matching the expected naming convention so it returns successfully. """ mock_rds_client.create_db_cluster_snapshot( DBClusterSnapshotIdentifier=f'{result}-shared', DBClusterIdentifier=mock_rds_cluster_name, ) with mock.patch.object( mock_rds_client, 'copy_db_cluster_snapshot', return_value={ 'DBClusterSnapshot': { 'DBClusterSnapshotIdentifier': f'{result}-shared', 'DBClusterIdentifier': 'string' } } ): shareable_snapshot = index.create_shareable_snapshot( 'cluster', mock_rds_client, result, mock_account_id ) assert shareable_snapshot == f'{result}-shared' @mock_aws def test_encrypted_with_default_key(mocker, mock_rds_cluster_name): """Test encrypted_with_default_key when database uses default key.""" kms_client = boto3.client('kms', region_name=config.AWS_DEFAULT_REGION) default_key = kms_client.describe_key(KeyId='alias/aws/rds') default_key_arn = default_key['KeyMetadata']['Arn'] mocker.patch( 'common.logic.database.get_config', return_value={'KmsKeyId': default_key_arn} ) result = index.encrypted_with_default_key( mock_rds_cluster_name, 'cluster', mock_rds_client, kms_client) assert result @mock_aws def test_encrypted_with_cmk(mocker, mock_rds_cluster_name): """Test encrypted_with_default_key when database uses CMK.""" kms_client = boto3.client('kms', region_name=config.AWS_DEFAULT_REGION) cmk = kms_client.create_key() key_arn = cmk['KeyMetadata']['Arn'] mocker.patch( 'common.logic.database.get_config', return_value={'KmsKeyId': key_arn} ) result = index.encrypted_with_default_key( mock_rds_cluster_name, 'cluster', mock_rds_client, kms_client) assert not result @mock_aws def test_unencrypted(mocker, mock_rds_cluster_name): """Test encrypted_with_default_key when database is unencrypted.""" kms_client = boto3.client('kms', region_name=config.AWS_DEFAULT_REGION) mocker.patch( 'common.logic.database.get_config', return_value={} ) result = index.encrypted_with_default_key( mock_rds_cluster_name, 'cluster', mock_rds_client, kms_client) assert not result @mock_aws def test_snapshot_encrypted_with_default_key_cluster(): """A cluster snapshot encrypted with the default key is detected.""" kms_client = boto3.client('kms', region_name=config.AWS_DEFAULT_REGION) default_key_arn = kms_client.describe_key( KeyId='alias/aws/rds')['KeyMetadata']['Arn'] rds_client = mock.Mock() rds_client.describe_db_cluster_snapshots.return_value = { 'DBClusterSnapshots': [{'KmsKeyId': default_key_arn}]} result = index.snapshot_encrypted_with_default_key( 'supplied-snapshot', 'cluster', rds_client, kms_client) assert result rds_client.describe_db_cluster_snapshots.assert_called_once_with( DBClusterSnapshotIdentifier='supplied-snapshot') @mock_aws def test_snapshot_encrypted_with_cmk_cluster(): """A cluster snapshot encrypted with a CMK is not flagged as default.""" kms_client = boto3.client('kms', region_name=config.AWS_DEFAULT_REGION) cmk_arn = kms_client.create_key()['KeyMetadata']['Arn'] rds_client = mock.Mock() rds_client.describe_db_cluster_snapshots.return_value = { 'DBClusterSnapshots': [{'KmsKeyId': cmk_arn}]} result = index.snapshot_encrypted_with_default_key( 'supplied-snapshot', 'cluster', rds_client, kms_client) assert not result @mock_aws def test_snapshot_encrypted_with_default_key_standalone(): """A standalone snapshot encrypted with the default key is detected.""" kms_client = boto3.client('kms', region_name=config.AWS_DEFAULT_REGION) default_key_arn = kms_client.describe_key( KeyId='alias/aws/rds')['KeyMetadata']['Arn'] rds_client = mock.Mock() rds_client.describe_db_snapshots.return_value = { 'DBSnapshots': [{'KmsKeyId': default_key_arn}]} result = index.snapshot_encrypted_with_default_key( 'supplied-snapshot', 'standalone', rds_client, kms_client) assert result rds_client.describe_db_snapshots.assert_called_once_with( DBSnapshotIdentifier='supplied-snapshot') @mock_aws def test_snapshot_encrypted_with_cmk_standalone(): """A standalone snapshot encrypted with a CMK is not flagged as default.""" kms_client = boto3.client('kms', region_name=config.AWS_DEFAULT_REGION) cmk_arn = kms_client.create_key()['KeyMetadata']['Arn'] rds_client = mock.Mock() rds_client.describe_db_snapshots.return_value = { 'DBSnapshots': [{'KmsKeyId': cmk_arn}]} result = index.snapshot_encrypted_with_default_key( 'supplied-snapshot', 'standalone', rds_client, kms_client) assert not result @mock_aws def test_main_supplied_snapshot_skips_creation(mocker, monkeypatch): """main() shares a supplied snapshot instead of creating a new one.""" monkeypatch.setattr('config.SOURCE_ACCOUNT_ID', '1234567890') monkeypatch.setattr('config.TARGET_ACCOUNT_ID', '9999999999') monkeypatch.setattr('config.RESTORE_SNAPSHOT_ID', 'supplied-snapshot') monkeypatch.setattr('config.TASK_TOKEN', None) mocker.patch( 'app.assume_source_account_role', return_value={ 'AccessKeyId': 'x', 'SecretAccessKey': 'y', 'SessionToken': 'z' }) mock_create_initial = mocker.patch('app.create_initial_snapshot') # A CMK-encrypted snapshot can be shared directly without a copy. mocker.patch('app.snapshot_encrypted_with_default_key', return_value=False) mock_create_shareable = mocker.patch('app.create_shareable_snapshot') mock_configure_sharing = mocker.patch( 'common.logic.database.configure_snapshot_sharing_settings') result = index.main() mock_create_initial.assert_not_called() mock_create_shareable.assert_not_called() mock_configure_sharing.assert_called_once_with( 'supplied-snapshot', '9999999999', config.DB_TYPE, client=mock.ANY) assert result == 'supplied-snapshot' @mock_aws def test_main_supplied_snapshot_default_key_creates_copy(mocker, monkeypatch): """A supplied default-key snapshot is copied to a CMK snapshot.""" monkeypatch.setattr('config.SOURCE_ACCOUNT_ID', '1234567890') monkeypatch.setattr('config.TARGET_ACCOUNT_ID', '9999999999') monkeypatch.setattr('config.RESTORE_SNAPSHOT_ID', 'supplied-snapshot') monkeypatch.setattr('config.TASK_TOKEN', None) mocker.patch( 'app.assume_source_account_role', return_value={ 'AccessKeyId': 'x', 'SecretAccessKey': 'y', 'SessionToken': 'z' }) mock_create_initial = mocker.patch('app.create_initial_snapshot') # A default-key snapshot must be copied to a CMK-encrypted snapshot so it # can be shared cross-account. mocker.patch('app.snapshot_encrypted_with_default_key', return_value=True) mock_create_shareable = mocker.patch( 'app.create_shareable_snapshot', return_value='supplied-snapshot-shared') mock_configure_sharing = mocker.patch( 'common.logic.database.configure_snapshot_sharing_settings') result = index.main() mock_create_initial.assert_not_called() mock_create_shareable.assert_called_once_with( config.DB_TYPE, mock.ANY, 'supplied-snapshot', '1234567890') mock_configure_sharing.assert_called_once_with( 'supplied-snapshot-shared', '9999999999', config.DB_TYPE, client=mock.ANY) assert result == 'supplied-snapshot-shared'