"""Test utils.""" import email from unittest.mock import patch import pandas as pd import pytest from src.utils import utils @patch('src.utils.utils.config.S3_EXPECTED_OWNER', '123456789012') @patch('src.utils.utils.s3_client') def test_upload_to_s3_success(mock_s3_client): """Test upload_to_s3 uploads file with correct bucket, key, filename, and ExpectedBucketOwner.""" utils.upload_to_s3('/path/to/file.xlsx', 'my-bucket', 'prefix/') mock_s3_client.upload_file.assert_called_once_with( Filename='/path/to/file.xlsx', Bucket='my-bucket', Key='prefix/file.xlsx', ExtraArgs={'ExpectedBucketOwner': '123456789012'}, ) @patch('src.utils.utils.config.S3_EXPECTED_OWNER', '123456789012') @patch('src.utils.utils.s3_client') def test_upload_to_s3_prefix_without_trailing_slash(mock_s3_client): """Test upload_to_s3 normalizes prefix without trailing slash.""" utils.upload_to_s3('/path/to/file.xlsx', 'my-bucket', 'prefix') mock_s3_client.upload_file.assert_called_once_with( Filename='/path/to/file.xlsx', Bucket='my-bucket', Key='prefix/file.xlsx', ExtraArgs={'ExpectedBucketOwner': '123456789012'}, ) @patch('src.utils.utils.config.S3_EXPECTED_OWNER', '123456789012') @patch('src.utils.utils.s3_client') def test_upload_to_s3_raises_exception(mock_s3_client): """Test upload_to_s3 re-raises boto3 exceptions.""" mock_s3_client.upload_file.side_effect = Exception('S3 upload failed') with pytest.raises(Exception, match='S3 upload failed'): utils.upload_to_s3('/path/to/file.xlsx', 'my-bucket', 'prefix/') @patch('src.utils.utils.config.S3_TMP_FILE_DIR', 'test-prefix/') @patch('src.utils.utils.config.S3_BUCKET', 'test-bucket') @patch('src.utils.utils.upload_to_s3') def test_export_and_upload_success(mock_upload_to_s3, tmp_path): """Test export_and_upload saves Excel file and uploads to S3.""" takedown_df = pd.DataFrame({'A': [1, 2]}) isrc_track_df = pd.DataFrame({'B': ['x', 'y']}) output_file = tmp_path / 'output.xlsx' utils.export_and_upload(takedown_df, isrc_track_df, str(output_file)) assert output_file.exists() mock_upload_to_s3.assert_called_once_with(str(output_file), 'test-bucket', 'test-prefix/') @patch('src.utils.utils.upload_to_s3') def test_export_and_upload_raises_exception(mock_upload_to_s3, tmp_path): """Test export_and_upload re-raises exceptions from upload_to_s3.""" mock_upload_to_s3.side_effect = Exception('Upload failed') takedown_df = pd.DataFrame({'A': [1, 2]}) isrc_track_df = pd.DataFrame({'B': ['x', 'y']}) output_file = tmp_path / 'output.xlsx' with pytest.raises(Exception, match='Upload failed'): utils.export_and_upload(takedown_df, isrc_track_df, str(output_file)) assert output_file.exists() # File should still be created even if upload fails @patch('src.utils.utils.config.SEND_UPCS_TO_MASTERS_REGISTRY', True) @patch('src.utils.utils.ses_client.send_raw_email') def test_send_success_email(mock_send_email, tmp_path): """Test send_success_email constructs email content correctly.""" test_file = tmp_path / 'testfile.xlsx' test_file.write_text('dummy content') utils.send_success_email(str(test_file), 'testfile.xlsx') mock_send_email.assert_called_once() call_kwargs = mock_send_email.call_args[1] assert call_kwargs['Source'] == utils.config.EMAIL_SENDER def test_send_success_email_file_not_found(): """Test send_success_email raises exception if file does not exist.""" with pytest.raises(FileNotFoundError): utils.send_success_email('nonexistent.xlsx', 'nonexistent.xlsx') def _decode_email_body(raw_email: str) -> str: """Extract and decode the base64-encoded plain text body from a raw MIME email string.""" msg = email.message_from_string(raw_email) for part in msg.walk(): if part.get_content_type() == 'text/plain': payload = part.get_payload(decode=True) return payload.decode('utf-8') if isinstance(payload, bytes) else payload return '' @patch('src.utils.utils.config.SEND_UPCS_TO_MASTERS_REGISTRY', False) @patch('src.utils.utils.ses_client.send_raw_email') def test_send_success_email_registry_skipped_note_present(mock_send_email, tmp_path): """Test send_success_email includes registry skipped note when SEND_UPCS_TO_MASTERS_REGISTRY is False.""" test_file = tmp_path / 'testfile.xlsx' test_file.write_text('dummy content') utils.send_success_email(str(test_file), 'testfile.xlsx') args, kwargs = mock_send_email.call_args body = _decode_email_body(kwargs['RawMessage']['Data']) assert 'Masters Registry update was skipped' in body @patch('src.utils.utils.config.SEND_UPCS_TO_MASTERS_REGISTRY', True) @patch('src.utils.utils.ses_client.send_raw_email') def test_send_success_email_registry_skipped_note_absent(mock_send_email, tmp_path): """Test send_success_email does not include registry skipped note when SEND_UPCS_TO_MASTERS_REGISTRY is True.""" test_file = tmp_path / 'testfile.xlsx' test_file.write_text('dummy content') utils.send_success_email(str(test_file), 'testfile.xlsx') args, kwargs = mock_send_email.call_args body = _decode_email_body(kwargs['RawMessage']['Data']) assert 'Masters Registry update was skipped' not in body @patch('src.utils.utils.config.SEND_UPCS_TO_MASTERS_REGISTRY', True) @patch('src.utils.utils.ses_client.send_raw_email') def test_send_success_email_attach_sheet(mock_send_email, tmp_path): """Test send_success_email creates correct MIME attachment.""" test_file = tmp_path / 'testfile.xlsx' test_file.write_text('dummy content') utils.send_success_email(str(test_file), 'testfile.xlsx') args, kwargs = mock_send_email.call_args email_message = kwargs['RawMessage']['Data'] # Normalize to string if bytes (SES API can accept both) if isinstance(email_message, bytes): email_message = email_message.decode('utf-8') assert 'Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' in email_message assert 'Content-Disposition: attachment; filename="testfile.xlsx"' in email_message