"""Lambda functional test module.""" from unittest.mock import MagicMock, mock_open, patch import hashlib from paramiko.ssh_exception import SSHException import pytest from src import app from src.sql_queries import UPDATE_HFA_ORCHARD_TRACK_LICENSES @patch('src.app.logger') @patch('src.app.open', new_callable=mock_open) @patch('src.app.s3.download_file_object') def test_download_file_from_s3_success(mock_s3_download_object, mock_file_open, mock_logger): """Test download_file_from_s3 function success.""" s3_key = 'test/key.txt' local_path = '/tmp/test.txt' mock_file = mock_file_open.return_value.__enter__.return_value app.download_file_from_s3(s3_key, local_path) mock_file_open.assert_called_once_with(local_path, 'wb') mock_s3_download_object.assert_called_once_with( 'mock-bucket', s3_key, mock_file ) mock_logger.info.assert_called_once_with( f'Successfully downloaded {s3_key} to {local_path}.' ) mock_logger.error.assert_not_called() @patch('src.app.logger') @patch('src.app.open', new_callable=mock_open) @patch('src.app.s3.download_file_object') def test_download_file_from_s3_exception(mock_s3_download_object, mock_file_open, mock_logger): """Test download_file_from_s3 function exception.""" s3_key = 'bad/key.txt' local_path = '/tmp/bad.txt' mock_file = mock_file_open.return_value.__enter__.return_value mock_s3_download_object.side_effect = Exception('S3 failure') with pytest.raises(Exception, match='S3 failure'): app.download_file_from_s3(s3_key, local_path) mock_file_open.assert_called_once_with(local_path, 'wb') mock_s3_download_object.assert_called_once_with( 'mock-bucket', s3_key, mock_file ) mock_logger.error.assert_called_once() mock_logger.info.assert_not_called() @patch('src.app.ZipFile') @patch('src.app.logger') def test_create_zip_success(mock_logger, mock_zipfile): """Test create_zip function success.""" zip_path = '/tmp/test.zip' source_path = '/tmp/source.txt' archive_name = 'source.txt' mock_zip_context = MagicMock() mock_zipfile.return_value.__enter__.return_value = mock_zip_context app.create_zip(zip_path, source_path, archive_name) mock_zipfile.assert_called_once_with(zip_path, 'w', app.ZIP_DEFLATED) mock_zip_context.write.assert_called_once_with(source_path, arcname=archive_name) mock_logger.info.assert_called_once_with( f'Successfully created ZIP file at {zip_path} containing {archive_name}.' ) mock_logger.error.assert_not_called() @patch('src.app.ZipFile', side_effect=Exception('Zip error')) @patch('src.app.logger') def test_create_zip_exception(mock_logger, mock_zipfile): """Test create_zip function exception.""" zip_path = '/tmp/bad.zip' source_path = '/tmp/source.txt' archive_name = 'source.txt' with pytest.raises(Exception, match='Zip error'): app.create_zip(zip_path, source_path, archive_name) mock_logger.error.assert_called_once() mock_logger.info.assert_not_called() @patch('src.app.pysftp.Connection') @patch('src.app.pysftp.CnOpts') @patch('src.app.logger') def test_upload_file_to_ftp_success(mock_logger, mock_cnopts, mock_connection): """Test upload_file_to_ftp function success.""" local_file = '/tmp/test.txt' remote_file = '/upload/test.txt' # Mock cnopts object mock_cnopts_instance = MagicMock() mock_cnopts.return_value = mock_cnopts_instance # Mock sftp connection context mock_sftp = MagicMock() mock_connection.return_value.__enter__.return_value = mock_sftp mock_sftp.listdir.return_value = [] mock_sftp.exists.return_value = True result = app.upload_file_to_ftp(local_file, remote_file) mock_connection.assert_called_once() mock_sftp.makedirs.assert_called_once_with(app.FTP_PATH) mock_sftp.put.assert_called_once_with(local_file, remote_file) mock_sftp.exists.assert_called_once_with(remote_file) mock_logger.info.assert_called_once_with('Connection successfully established.') assert result is True @patch('src.app.pysftp.Connection') @patch('src.app.pysftp.CnOpts') @patch('src.app.logger') def test_upload_file_to_ftp_ssh_failure(mock_logger, mock_cnopts, mock_connection): """Test upload_file_to_ftp raises SSHException with contact info.""" local_file = '/tmp/test.txt' remote_file = '/upload/test.txt' mock_cnopts.return_value = MagicMock() mock_connection.side_effect = SSHException('SFTP handshake failed') with pytest.raises(RuntimeError) as exec_info: app.upload_file_to_ftp(local_file, remote_file) mock_logger.error.assert_called_once_with('SFTP connection failed due to SSH error: SFTP handshake failed') assert app.mail.HFA_SFTP_CONTACT_INFO in str(exec_info.value) @patch('src.app.pysftp.Connection') @patch('src.app.pysftp.CnOpts') @patch('src.app.logger') def test_upload_file_to_ftp_failure(mock_logger, mock_cnopts, mock_connection): """Test upload_file_to_ftp function exception.""" local_file = '/tmp/test.txt' remote_file = '/upload/test.txt' # Mock cnopts mock_cnopts.return_value = MagicMock() # Simulate exception on connection mock_connection.side_effect = Exception('Connection failed') with pytest.raises(Exception, match='Connection failed'): app.upload_file_to_ftp(local_file, remote_file) mock_logger.error.assert_called_once() @patch('src.app.logger') @patch('src.app.s3.upload_file_to_s3') def test_upload_zip_to_s3_success(mock_s3_upload, mock_logger): """Test upload_zip_to_s3 function success.""" file_name = 'test.zip' file_path = '/tmp/test.zip' app.upload_zip_to_s3(file_name, file_path) expected_s3_key = f'{app.S3_REQUEST_ZIP_DIR}{file_name}' mock_s3_upload.assert_called_once_with( app.S3_BUCKET_NAME, file_path, expected_s3_key ) mock_logger.info.assert_called_once_with( f'Uploaded {file_name} to S3: s3://{app.S3_BUCKET_NAME}/{expected_s3_key}' ) mock_logger.error.assert_not_called() @patch('src.app.logger') @patch('src.app.s3.upload_file_to_s3') def test_upload_zip_to_s3_exception(mock_s3_upload, mock_logger): """Test create_zip function exception.""" file_name = 'fail.zip' file_path = '/tmp/fail.zip' mock_s3_upload.side_effect = Exception('Upload error') with pytest.raises(Exception, match='Upload error'): app.upload_zip_to_s3(file_name, file_path) expected_s3_key = f'{app.S3_REQUEST_ZIP_DIR}{file_name}' mock_s3_upload.assert_called_once_with( app.S3_BUCKET_NAME, file_path, expected_s3_key ) mock_logger.error.assert_called_once() mock_logger.info.assert_not_called() @patch('src.app.logger') @patch('src.app.pd.read_csv') def test_transform_license_data_success(mock_read_csv, mock_logger, mock_orchard_license_data_df): """Test transform_license_data success.""" file_path = '/tmp/test.txt' file_name = 'test.txt' mock_read_csv.return_value = mock_orchard_license_data_df df = app.transform_license_data(file_path, file_name) # Ensure file read was attempted mock_read_csv.assert_called_once_with( file_path, sep='\t', names=app.file_fields.FIELDS, quoting=app.csv.QUOTE_NONE, escapechar='\\' ) # Check that transformation happened correctly assert 'request_file_name' in df.columns assert df['request_file_name'].iloc[0] == file_name assert df['user_defined_6'].iloc[0] == 'TRUE' assert df['user_defined_6'].iloc[3] == 'FALSE' mock_logger.info.assert_any_call(f'Reading file: {file_path}') mock_logger.info.assert_any_call('Transformation complete') mock_logger.error.assert_not_called() @patch('src.app.logger') @patch('src.app.pd.read_csv') def test_transform_license_data_failure(mock_read_csv, mock_logger): """Test transform_license_data exception.""" file_path = '/tmp/bad.txt' file_name = 'bad.txt' mock_read_csv.side_effect = Exception('Parsing failed') with pytest.raises(Exception, match='Parsing failed'): app.transform_license_data(file_path, file_name) mock_logger.error.assert_called_once() mock_logger.info.assert_called_once_with(f'Reading file: {file_path}') @patch('src.app.logger') @patch('src.app.util.mysql_connection') def test_update_hfa_orchard_track_licenses_state_success( mock_mysql_connection, mock_logger, mock_orchard_license_data_df ): """Test update_hfa_orchard_track_licenses_state success.""" # Mock connection and cursor mock_conn = MagicMock() mock_cursor = MagicMock() mock_cursor.rowcount = 3 mock_conn.cursor.return_value.__enter__.return_value = mock_cursor mock_mysql_connection.return_value.__enter__.return_value = mock_conn expected_query = UPDATE_HFA_ORCHARD_TRACK_LICENSES.format(placeholders='%s, %s, %s') app.update_hfa_orchard_track_licenses_state(mock_orchard_license_data_df) mock_logger.info.assert_any_call('Updating 3 track licenses state in the database.') mock_logger.info.assert_any_call('Updated state for 3 track license(s).') mock_cursor.execute.assert_called_once_with(expected_query, [1, 2, 3]) mock_conn.commit.assert_called_once() mock_logger.error.assert_not_called() @patch('src.app.logger') @patch('src.app.util.mysql_connection') def test_update_hfa_orchard_track_licenses_state_failure( mock_mysql_connection, mock_logger, mock_orchard_license_data_df ): """Test update_hfa_orchard_track_licenses_state failure.""" mock_mysql_connection.side_effect = Exception('DB failure') with pytest.raises(Exception, match='DB failure'): app.update_hfa_orchard_track_licenses_state(mock_orchard_license_data_df) mock_logger.error.assert_called_once() mock_logger.info.assert_called_once_with('Updating 3 track licenses state in the database.') @patch('src.app.logger') @patch('src.app.engine') def test_insert_hfa_license_request_success(mock_engine, mock_logger, mock_orchard_license_data_df): """Test insert_hfa_license_request success.""" mock_conn = MagicMock() mock_engine.connect.return_value.__enter__.return_value = mock_conn with patch.object(mock_orchard_license_data_df, 'to_sql', return_value=4) as mock_to_sql: result = app.insert_hfa_license_request(mock_orchard_license_data_df) mock_to_sql.assert_called_once_with( 'hfa_license_requests', con=mock_engine, if_exists='append', index=False ) assert result == 4 mock_logger.info.assert_any_call('Inserting 4 records into hfa_license_requests.') mock_logger.info.assert_any_call('Inserted 4 records into hfa_license_requests.') mock_logger.error.assert_not_called() @patch('src.app.logger') @patch('src.app.engine') def test_insert_hfa_license_request_failure(mock_engine, mock_logger, mock_orchard_license_data_df): """Test insert_hfa_license_request failure.""" with patch.object(mock_orchard_license_data_df, 'to_sql', side_effect=Exception('DB write failed')): with pytest.raises(Exception, match='DB write failed'): app.insert_hfa_license_request(mock_orchard_license_data_df) mock_logger.info.assert_called_once_with('Inserting 4 records into hfa_license_requests.') mock_logger.error.assert_called_once() @patch('src.app.logger') @patch('src.app.ses.send_email') @patch('src.app.os.path.getsize') @patch('src.app.datetime') def test_send_success_email_success(mock_datetime, mock_getsize, mock_send_email, mock_logger): """Test send_success_email success.""" filepath = '/fake/path/data.zip' filename = 'data.zip' row_count = 123 mock_now_str = 'Thu May 15 10:00:00 UTC 2025' mock_datetime.now.return_value.astimezone.return_value.strftime.return_value = mock_now_str mock_getsize.return_value = 10240 app.send_success_email(filepath, filename, row_count) expected_md5 = hashlib.md5(filename.encode('utf-8')).hexdigest() expected_message = app.mail.MESSAGE_BODY.format( filename=filename, number_of_rows=row_count, md5=expected_md5, file_size='10 KB', current_time=mock_now_str ) expected_subject = 'The Orchard - Upload HFA Request Files (test)' mock_send_email.assert_called_once_with( recipients=['test@example.com'], sender='sender@example.com', subject=expected_subject, message=expected_message ) mock_logger.info.assert_called_once_with(f'Success email sent for file: {filename}') @patch('src.app.logger') @patch('src.app.ses.send_email') @patch('src.app.os.path.getsize') @patch('src.app.datetime') def test_send_success_email_failure(mock_datetime, mock_getsize, mock_send_email, mock_logger): """Test send_success_email failure.""" filepath = '/fake/path/failure.zip' filename = 'failure.zip' row_count = 5 mock_now_str = 'Thu May 15 10:00:00 UTC 2025' mock_datetime.now.return_value.astimezone.return_value.strftime.return_value = mock_now_str mock_getsize.return_value = 10240 mock_send_email.side_effect = Exception('SMTP failure') with pytest.raises(Exception, match='SMTP failure'): app.send_success_email(filepath, filename, row_count) mock_logger.error.assert_called_once()