"""Lambda test module.""" import os import tempfile from unittest.mock import MagicMock, patch import boto3 import pytest from moto import mock_aws import config from src import app @pytest.fixture() def mock_account_id(): """Return an AWS account id.""" return "123456789000" @pytest.fixture() def mock_db_name(): """Return a test database name.""" return "test-db" @pytest.fixture() def mock_cluster_identifier(): """Return a test cluster identifier.""" return "test-cluster" @pytest.fixture() def mock_parameter_group(): """Return a test parameter group name.""" return "test-param-group" @pytest.fixture() def mock_sts_credentials(): """Return mock STS credentials.""" return { "AccessKeyId": "AKIAIOSFODNN7EXAMPLE", "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", "SessionToken": "FwoGZXIvYXdzEBYaDH", "Expiration": "2024-01-01T00:00:00Z", } @pytest.fixture() def mock_db_credentials(): """Return mock database credentials.""" return { "host": "test-host.amazonaws.com", "username": "testuser", "password": "testpassword", } @pytest.fixture() def mock_mysql_event(mock_account_id): """Return a mock Lambda event.""" return { "db_name": "test-db", "db_type": "cluster", "source_account_id": mock_account_id, } @pytest.fixture() def mock_postgresql_event(mock_account_id): """Return a mock Lambda event.""" return { "db_name": "test-postgres-db", "db_type": "cluster", "source_account_id": mock_account_id, } @pytest.fixture() def mock_context(): """Return a mock Lambda context.""" context = MagicMock() context.function_name = "extract_native_configuration" context.aws_request_id = "test-request-id" return context @pytest.fixture() @mock_aws def mock_rds_client(): """Return a mock RDS client.""" client = boto3.client("rds", region_name=config.AWS_DEFAULT_REGION) return client def test_assume_source_account_role( mock_account_id, mock_sts_credentials, monkeypatch ): """Test assume_source_account_role function.""" # Mock the expected STS assume_role response mock_assume_role_response = {"Credentials": mock_sts_credentials} # Patch the STS client mock_sts_client = MagicMock() mock_sts_client.assume_role.return_value = mock_assume_role_response monkeypatch.setattr(app.config, "EXTERNAL_ID", "abc1234") monkeypatch.setattr(app.config, "SERVICE_NAME", "test-service") with patch("boto3.client", return_value=mock_sts_client): result = app.assume_source_account_role(mock_account_id, "test-role") # Verify the STS client was called with correct parameters mock_sts_client.assume_role.assert_called_once_with( RoleArn=f"arn:aws:iam::{mock_account_id}:role/test-role", RoleSessionName="test-service", ExternalId="abc1234", DurationSeconds=3600, ) # Verify the returned credentials match expected values assert result == mock_sts_credentials assert result["AccessKeyId"] == "AKIAIOSFODNN7EXAMPLE" assert result["SecretAccessKey"] == "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY" assert result["SessionToken"] == "FwoGZXIvYXdzEBYaDH" @mock_aws def test_get_cluster_parameter_group_name_success( mock_cluster_identifier, mock_parameter_group, mock_rds_client ): """Test successful retrieval of cluster parameter group name.""" # Create a mock cluster mock_rds_client.create_db_cluster( DBClusterIdentifier=mock_cluster_identifier, Engine="aurora-mysql", MasterUsername="testuser", MasterUserPassword="testpass", DBClusterParameterGroupName=mock_parameter_group, ) result = app.get_cluster_parameter_group_name( mock_cluster_identifier, mock_rds_client ) assert result == mock_parameter_group @mock_aws def test_get_cluster_parameter_group_name_no_clusters(mock_rds_client): """Test get_cluster_parameter_group_name when no clusters exist.""" # This should raise an exception due to cluster not existing with pytest.raises(Exception): app.get_cluster_parameter_group_name( "non-existent-cluster", mock_rds_client ) @mock_aws def test_get_modified_parameters_with_user_parameters( mock_parameter_group, monkeypatch, mock_rds_client ): """Test getting modified parameters with user-defined parameters.""" # Mock the paginator and response mock_paginator = MagicMock() mock_page = { "Parameters": [ { "ParameterName": "max_connections", "ParameterValue": "1000", "Source": "user", }, { "ParameterName": "innodb_buffer_pool_size", "ParameterValue": "8G", "Source": "system", }, { "ParameterName": "slow_query_log", "ParameterValue": "ON", "Source": "user", }, ] } mock_paginator.paginate.return_value = [mock_page] mock_rds_client.get_paginator = MagicMock(return_value=mock_paginator) # Mock config to avoid filtering monkeypatch.setattr(config, "OMITTED_PARAMETER_GROUP_PATTERNS", {}) with patch("boto3.client", return_value=mock_rds_client): result = app.get_modified_parameters( "mysql", mock_parameter_group, mock_rds_client ) assert len(result) == 2 assert result[0]["ParameterName"] == "max_connections" assert result[0]["ParameterValue"] == "1000" assert result[1]["ParameterName"] == "slow_query_log" assert result[1]["ParameterValue"] == "ON" @mock_aws def test_get_modified_parameters_with_rename( mock_parameter_group, monkeypatch, mock_rds_client ): """Test getting modified parameters with user-defined parameters.""" # Mock the paginator and response mock_paginator = MagicMock() mock_page = { "Parameters": [ { "ParameterName": "time_zone", "ParameterValue": "UTC", "Source": "user", }, ] } mock_paginator.paginate.return_value = [mock_page] mock_rds_client.get_paginator = MagicMock(return_value=mock_paginator) monkeypatch.setattr(config, "OMITTED_PARAMETER_GROUP_PATTERNS", {}) monkeypatch.setattr( config, "RENAMED_PARAMETER_GROUP_NAMES", {"mysql": {"time_zone": "default_time_zone"}}, ) with patch("boto3.client", return_value=mock_rds_client): result = app.get_modified_parameters( "mysql", mock_parameter_group, mock_rds_client ) assert len(result) == 1 assert result[0]["ParameterName"] == "default_time_zone" assert result[0]["ParameterValue"] == "UTC" @mock_aws def test_get_modified_parameters_with_required( mock_parameter_group, mock_rds_client ): """Test getting modified parameters including required parameters.""" mock_paginator = MagicMock() mock_page = { "Parameters": [ { "ParameterName": "sql_mode", "ParameterValue": "0", "Source": "system", }, ] } mock_paginator.paginate.return_value = [mock_page] mock_rds_client.get_paginator = MagicMock(return_value=mock_paginator) with patch("boto3.client", return_value=mock_rds_client): result = app.get_modified_parameters( "mysql", mock_parameter_group, mock_rds_client ) assert len(result) == 1 assert result[0]["ParameterName"] == "sql_mode" @mock_aws def test_get_modified_parameters_with_omitted_patterns( mock_parameter_group, monkeypatch, mock_rds_client ): """Test getting modified parameters with some parameters omitted.""" mock_paginator = MagicMock() mock_page = { "Parameters": [ { "ParameterName": "max_connections", "ParameterValue": "1000", "Source": "user", }, { "ParameterName": "binlog_format", "ParameterValue": "ROW", "Source": "user", }, ] } mock_paginator.paginate.return_value = [mock_page] mock_rds_client.get_paginator = MagicMock(return_value=mock_paginator) # Mock config to filter out binlog parameters monkeypatch.setattr( config, "OMITTED_PARAMETER_GROUP_PATTERNS", {"mysql": ("binlog_",)} ) with patch("boto3.client", return_value=mock_rds_client): result = app.get_modified_parameters( "mysql", mock_parameter_group, mock_rds_client ) assert len(result) == 1 assert result[0]["ParameterName"] == "max_connections" def test_write_modified_parameters_to_file(): """Test writing modified parameters to a file.""" modified_params = [ {"ParameterName": "max_connections", "ParameterValue": "1000"}, {"ParameterName": "slow_query_log", "ParameterValue": "ON"}, ] with tempfile.TemporaryDirectory() as temp_dir: original_cwd = os.getcwd() os.chdir(temp_dir) try: result = app.write_modified_parameters_to_file( modified_params, "mysql" ) assert result == "/tmp/my.cnf" with open(result, "r") as f: content = f.read().split("\n") assert len(content) == 4 assert content[0] == "[mysqld]" assert content[1] == "max_connections=1000" assert content[2] == "slow_query_log=ON" assert content[3] == "" finally: os.chdir(original_cwd) def test_write_modified_parameters_to_file_empty_params(): """Test writing empty parameters list to file.""" modified_params = [] with tempfile.TemporaryDirectory() as temp_dir: original_cwd = os.getcwd() os.chdir(temp_dir) try: result = app.write_modified_parameters_to_file( modified_params, "mysql" ) assert result == "/tmp/my.cnf" with open(result, "r") as f: content = f.read() assert content == "" finally: os.chdir(original_cwd) @mock_aws def test_handler_mysql_success( mock_mysql_event, mock_context, mock_parameter_group, mock_rds_client, mock_sts_credentials, monkeypatch, ): """Test successful handler execution for MySQL database.""" # Setup S3 s3_client = boto3.client("s3", region_name="us-east-1") s3_client.create_bucket(Bucket="test-bucket") # Setup RDS mock_rds_client.create_db_cluster( DBClusterIdentifier="test-db", Engine="aurora-mysql", MasterUsername="testuser", MasterUserPassword="testpass", DBClusterParameterGroupName=mock_parameter_group, ) # Mock dependencies mock_db_credentials = { "host": "test-host", "username": "user", "password": "pass", } mock_db_config = {"Engine": "aurora-mysql", "EngineVersion": "8.0.35"} monkeypatch.setattr(config, "S3_BUCKET_NAME", "test-bucket") # Mock boto3.client to return mock_rds_client for RDS calls original_boto3_client = boto3.client def mock_client_factory(service_name, **kwargs): if service_name == "rds": return mock_rds_client return original_boto3_client(service_name, **kwargs) with ( patch("boto3.client", side_effect=mock_client_factory), patch( "src.app.assume_source_account_role", return_value=mock_sts_credentials, ), patch( "common.logic.database.reset_rds_master_credentials", return_value=mock_db_credentials, ), patch("common.logic.database.get_config", return_value=mock_db_config), patch("src.logic.mysql.get_database_ddl", return_value="schema.sql"), patch( "src.app.get_cluster_parameter_group_name", return_value=mock_parameter_group, ), patch("src.app.get_modified_parameters", return_value=[]), patch("os.path.exists", return_value=True), ): with tempfile.NamedTemporaryFile( mode="w", suffix=".sql", delete=False, encoding="utf-8" ) as temp_file: temp_file.write("-- Test schema\n") temp_file.flush() schema_file = temp_file.name try: with patch( "src.logic.mysql.get_database_ddl", return_value=schema_file ): result = app.handler(mock_mysql_event, mock_context) assert result == {"status": "completed"} finally: os.unlink(schema_file) @mock_aws def test_handler_postgresql_success( mock_context, mock_parameter_group, mock_rds_client, mock_postgresql_event, mock_sts_credentials, monkeypatch, ): """Test successful handler execution for PostgreSQL database.""" # Setup S3 s3_client = boto3.client("s3", region_name="us-east-1") s3_client.create_bucket(Bucket="test-bucket") # Setup RDS mock_rds_client.create_db_cluster( DBClusterIdentifier="test-postgres-db", Engine="aurora-postgresql", MasterUsername="testuser", MasterUserPassword="testpass", DBClusterParameterGroupName=mock_parameter_group, ) # Mock dependencies mock_db_credentials = { "host": "test-host", "username": "user", "password": "pass", } mock_db_config = {"Engine": "aurora-postgresql", "EngineVersion": "15.4"} monkeypatch.setattr(config, "S3_BUCKET_NAME", "test-bucket") # Mock boto3.client to return mock_rds_client for RDS calls original_boto3_client = boto3.client def mock_client_factory(service_name, **kwargs): if service_name == "rds": return mock_rds_client return original_boto3_client(service_name, **kwargs) with ( patch("boto3.client", side_effect=mock_client_factory), patch( "src.app.assume_source_account_role", return_value=mock_sts_credentials, ), patch( "common.logic.database.reset_rds_master_credentials", return_value=mock_db_credentials, ), patch("common.logic.database.get_config", return_value=mock_db_config), patch("src.logic.postgresql.get_database_ddl", return_value="schema.sql"), patch( "src.app.get_cluster_parameter_group_name", return_value=mock_parameter_group, ), patch("src.app.get_modified_parameters", return_value=[]), patch("os.path.exists", return_value=True), ): with tempfile.NamedTemporaryFile( mode="w", suffix=".sql", delete=False ) as temp_file: temp_file.write("-- Test PostgreSQL schema\n") schema_file = temp_file.name try: with patch( "src.logic.postgresql.get_database_ddl", return_value=schema_file ): result = app.handler(mock_postgresql_event, mock_context) assert result == {"status": "completed"} finally: os.unlink(schema_file) def test_handler_missing_parameters(mock_account_id, mock_context): """Test handler with missing required parameters.""" event = {"source_account_id": mock_account_id} # Missing db_name and db_type with pytest.raises( ValueError, match="Missing 'db_name', 'db_type', or 'source_account_id' in payload", ): app.handler(event, mock_context) @mock_aws def test_handler_with_modified_parameters( mock_mysql_event, mock_context, mock_sts_credentials, monkeypatch ): """Test handler execution with modified parameters.""" # Setup S3 s3_client = boto3.client("s3", region_name="us-east-1") s3_client.create_bucket(Bucket="test-bucket") # Setup RDS rds_client = boto3.client("rds", region_name="us-east-1") rds_client.create_db_cluster( DBClusterIdentifier="test-db", Engine="aurora-mysql", MasterUsername="testuser", MasterUserPassword="testpass", DBClusterParameterGroupName="test-param-group", ) # Mock dependencies mock_db_credentials = { "host": "test-host", "username": "user", "password": "pass", } mock_db_config = {"Engine": "aurora-mysql", "EngineVersion": "8.0.35"} mock_modified_params = [ {"ParameterName": "max_connections", "ParameterValue": "1000"} ] monkeypatch.setattr(config, "S3_BUCKET_NAME", "test-bucket") with ( patch( "src.app.assume_source_account_role", return_value=mock_sts_credentials, ), patch( "common.logic.database.reset_rds_master_credentials", return_value=mock_db_credentials, ), patch("common.logic.database.get_config", return_value=mock_db_config), patch("src.logic.mysql.get_database_ddl", return_value="schema.sql"), patch( "src.app.get_cluster_parameter_group_name", return_value="test-param-group", ), patch( "src.app.get_modified_parameters", return_value=mock_modified_params ), patch( "src.app.write_modified_parameters_to_file", return_value="/tmp/my.cnf", ), patch("os.path.exists", return_value=True), ): with tempfile.NamedTemporaryFile( mode="w", suffix=".sql", delete=False ) as temp_file: temp_file.write("-- Test schema\n") schema_file = temp_file.name with tempfile.NamedTemporaryFile( mode="w", suffix=".cnf", delete=False ) as config_file: config_file.write("max_connections=1000\n") options_file = config_file.name try: with ( patch( "src.logic.mysql.get_database_ddl", return_value=schema_file ), patch( "src.app.write_modified_parameters_to_file", return_value=options_file, ), ): result = app.handler(mock_mysql_event, mock_context) assert result == {"status": "completed"} finally: os.unlink(schema_file) os.unlink(options_file)