"""Lambda test module.""" import json from unittest.mock import MagicMock, patch import pytest from src.app import ( BadRow, InputRow, generate_bad_rows_csv_to_s3, generate_manifest_json_to_s3, generate_pdp_csv_to_s3, parse_s3_event, process_row, validate_row, ) class TestParseS3Event: def test_parse_s3_event_success(self): event = { "detail": { "bucket": {"name": "test-bucket"}, "object": {"key": "test/key.csv"}, } } bucket, key = parse_s3_event(event) assert bucket == "test-bucket" assert key == "test/key.csv" def test_parse_s3_event_missing_detail(self): event = {} with pytest.raises(ValueError, match="Invalid EventBridge S3 event structure"): parse_s3_event(event) def test_parse_s3_event_missing_bucket(self): event = {"detail": {"object": {"key": "test/key.csv"}}} with pytest.raises(ValueError, match="Invalid EventBridge S3 event structure"): parse_s3_event(event) class TestValidateRow: def test_validate_row_success(self): row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="abc-123", ) assert validate_row(row) is None def test_validate_row_empty_identity_id(self): row = InputRow( identity_id="", tenant_type="parent_company", tenant_uuid="abc-123", ) assert validate_row(row) == "identity_id is empty" def test_validate_row_empty_tenant_type(self): row = InputRow( identity_id="12345", tenant_type="", tenant_uuid="abc-123", ) assert validate_row(row) == "tenant_type is empty" def test_validate_row_empty_tenant_uuid(self): row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="", ) assert validate_row(row) == "tenant_uuid is empty" def test_validate_row_invalid_tenant_type(self): row = InputRow( identity_id="12345", tenant_type="invalid_type", tenant_uuid="abc-123", ) assert validate_row(row) == "invalid tenant_type: invalid_type" def test_validate_row_all_valid_tenant_types(self): valid_types = ["parent_company", "company_brand", "account"] for tenant_type in valid_types: row = InputRow( identity_id="12345", tenant_type=tenant_type, tenant_uuid="abc-123", ) assert validate_row(row) is None class TestProcessRow: @patch("src.app.create_songwhip_profile") @patch("src.app.check_tenant_exists") @patch("src.app.check_identity_exists") def test_process_row_success( self, mock_identity_exists, mock_tenant_exists, mock_create_profile ): mock_identity_exists.return_value = True mock_tenant_exists.return_value = True mock_create_profile.return_value = True row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="abc-123", ) session = MagicMock() result = process_row(row, session) assert result is None @patch("src.app.check_identity_exists") def test_process_row_identity_not_found(self, mock_identity_exists): mock_identity_exists.return_value = False row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="abc-123", ) session = MagicMock() result = process_row(row, session) assert isinstance(result, BadRow) assert result.reason == "identity_not_found" @patch("src.app.check_tenant_exists") @patch("src.app.check_identity_exists") def test_process_row_tenant_not_found( self, mock_identity_exists, mock_tenant_exists ): mock_identity_exists.return_value = True mock_tenant_exists.return_value = False row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="abc-123", ) session = MagicMock() result = process_row(row, session) assert isinstance(result, BadRow) assert result.reason == "tenant_not_found" def test_process_row_validation_error(self): row = InputRow( identity_id="", tenant_type="parent_company", tenant_uuid="abc-123", ) session = MagicMock() result = process_row(row, session) assert isinstance(result, BadRow) assert result.reason == "identity_id is empty" @patch("src.app.create_songwhip_profile") @patch("src.app.check_tenant_exists") @patch("src.app.check_identity_exists") def test_process_row_profile_creation_failed( self, mock_identity_exists, mock_tenant_exists, mock_create_profile ): mock_identity_exists.return_value = True mock_tenant_exists.return_value = True mock_create_profile.return_value = False row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="abc-123", ) session = MagicMock() result = process_row(row, session) assert isinstance(result, BadRow) assert result.reason == "profile_creation_failed" class TestInputRow: def test_input_row_dataclass(self): row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="abc-123", ) assert row.identity_id == "12345" assert row.tenant_type == "parent_company" assert row.tenant_uuid == "abc-123" class TestBadRow: def test_bad_row_dataclass(self): row = InputRow( identity_id="12345", tenant_type="parent_company", tenant_uuid="abc-123", ) bad_row = BadRow(row=row, reason="test_reason") assert bad_row.row == row assert bad_row.reason == "test_reason" class TestGenerateManifestJsonToS3: @patch("src.app.boto3.client") @patch("src.app.config") def test_generate_manifest_json_to_s3_success(self, mock_config, mock_boto3_client): mock_config.ENVIRONMENT = "qa" mock_config.SONGWHIP_OUTPUT_FOLDER = "songwhip_output/" mock_s3_client = MagicMock() mock_boto3_client.return_value = mock_s3_client timestamp = "2024-01-15T10-30-00" bucket_name = "test-bucket" result = generate_manifest_json_to_s3(timestamp, bucket_name) assert result == f"s3://{bucket_name}/songwhip_output/{timestamp}/manifest.json" mock_boto3_client.assert_called_once_with("s3") mock_s3_client.put_object.assert_called_once() call_kwargs = mock_s3_client.put_object.call_args.kwargs assert call_kwargs["Bucket"] == bucket_name assert call_kwargs["Key"] == f"songwhip_output/{timestamp}/manifest.json" assert call_kwargs["ContentType"] == "application/json" uploaded_manifest = json.loads(call_kwargs["Body"]) assert uploaded_manifest == { "bucket": "qa-pdp-backfill", "jobs": [ { "job_type": "attach_and_detach", "keys": [f"songwhip-backfill/{timestamp}.csv"], } ], } @patch("src.app.boto3.client") @patch("src.app.config") def test_generate_manifest_json_to_s3_prod_environment( self, mock_config, mock_boto3_client ): mock_config.ENVIRONMENT = "prod" mock_config.SONGWHIP_OUTPUT_FOLDER = "songwhip_output/" mock_s3_client = MagicMock() mock_boto3_client.return_value = mock_s3_client timestamp = "2024-01-15T10-30-00" bucket_name = "prod-bucket" result = generate_manifest_json_to_s3(timestamp, bucket_name) assert result == f"s3://{bucket_name}/songwhip_output/{timestamp}/manifest.json" call_kwargs = mock_s3_client.put_object.call_args.kwargs uploaded_manifest = json.loads(call_kwargs["Body"]) assert uploaded_manifest["bucket"] == "prod-pdp-backfill" class TestGeneratePdpCsvToS3: @patch("src.app.boto3.client") @patch("src.app.config") def test_generate_pdp_csv_to_s3_success(self, mock_config, mock_boto3_client): mock_config.SONGWHIP_OUTPUT_FOLDER = "songwhip_output/" mock_s3_client = MagicMock() mock_boto3_client.return_value = mock_s3_client rows = [ InputRow( identity_id="identity-1", tenant_type="parent_company", tenant_uuid="tenant-1", ), InputRow( identity_id="identity-2", tenant_type="company_brand", tenant_uuid="tenant-2", ), ] timestamp = "2024-01-15T10-30-00" bucket_name = "test-bucket" result_url, result_timestamp = generate_pdp_csv_to_s3( rows, bucket_name, timestamp ) assert ( result_url == f"s3://{bucket_name}/songwhip_output/{timestamp}/{timestamp}.csv" ) assert result_timestamp == timestamp mock_boto3_client.assert_called_once_with("s3") mock_s3_client.put_object.assert_called_once() call_kwargs = mock_s3_client.put_object.call_args.kwargs assert call_kwargs["Bucket"] == bucket_name assert call_kwargs["Key"] == f"songwhip_output/{timestamp}/{timestamp}.csv" assert call_kwargs["ContentType"] == "text/csv" csv_content = call_kwargs["Body"] lines = csv_content.strip().splitlines() assert len(lines) == 3 # header + 2 rows assert lines[0] == "identity_uuid,tenant_uuid,tenant_type,role,operation" assert lines[1] == "identity-1,tenant-1,parent_company,songwhip_read,attach" assert lines[2] == "identity-2,tenant-2,company_brand,songwhip_read,attach" @patch("src.app.boto3.client") @patch("src.app.config") def test_generate_pdp_csv_to_s3_empty_rows(self, mock_config, mock_boto3_client): mock_config.SONGWHIP_OUTPUT_FOLDER = "songwhip_output/" mock_s3_client = MagicMock() mock_boto3_client.return_value = mock_s3_client rows: list[InputRow] = [] timestamp = "2024-01-15T10-30-00" bucket_name = "test-bucket" result_url, result_timestamp = generate_pdp_csv_to_s3( rows, bucket_name, timestamp ) assert ( result_url == f"s3://{bucket_name}/songwhip_output/{timestamp}/{timestamp}.csv" ) call_kwargs = mock_s3_client.put_object.call_args.kwargs csv_content = call_kwargs["Body"] lines = csv_content.strip().splitlines() assert len(lines) == 1 # header only assert lines[0] == "identity_uuid,tenant_uuid,tenant_type,role,operation" class TestGenerateBadRowsCsvToS3: @patch("src.app.boto3.client") @patch("src.app.config") def test_generate_bad_rows_csv_to_s3_success(self, mock_config, mock_boto3_client): mock_config.SONGWHIP_OUTPUT_FOLDER = "songwhip_output/" mock_s3_client = MagicMock() mock_boto3_client.return_value = mock_s3_client bad_rows = [ BadRow( row=InputRow( identity_id="identity-1", tenant_type="parent_company", tenant_uuid="tenant-1", ), reason="identity_not_found", ), BadRow( row=InputRow( identity_id="identity-2", tenant_type="company_brand", tenant_uuid="tenant-2", ), reason="tenant_not_found", ), ] timestamp = "2024-01-15T10-30-00" bucket_name = "test-bucket" result = generate_bad_rows_csv_to_s3(bad_rows, bucket_name, timestamp) assert ( result == f"s3://{bucket_name}/songwhip_output/{timestamp}/failed_rows.csv" ) mock_boto3_client.assert_called_once_with("s3") mock_s3_client.put_object.assert_called_once() call_kwargs = mock_s3_client.put_object.call_args.kwargs assert call_kwargs["Bucket"] == bucket_name assert call_kwargs["Key"] == f"songwhip_output/{timestamp}/failed_rows.csv" assert call_kwargs["ContentType"] == "text/csv" csv_content = call_kwargs["Body"] lines = csv_content.strip().splitlines() assert len(lines) == 3 # header + 2 rows assert lines[0] == "identity_id,tenant_type,tenant_uuid,error_reason" assert lines[1] == "identity-1,parent_company,tenant-1,identity_not_found" assert lines[2] == "identity-2,company_brand,tenant-2,tenant_not_found" @patch("src.app.boto3.client") @patch("src.app.config") def test_generate_bad_rows_csv_to_s3_empty_rows( self, mock_config, mock_boto3_client ): mock_config.SONGWHIP_OUTPUT_FOLDER = "songwhip_output/" mock_s3_client = MagicMock() mock_boto3_client.return_value = mock_s3_client bad_rows: list[BadRow] = [] timestamp = "2024-01-15T10-30-00" bucket_name = "test-bucket" result = generate_bad_rows_csv_to_s3(bad_rows, bucket_name, timestamp) assert ( result == f"s3://{bucket_name}/songwhip_output/{timestamp}/failed_rows.csv" ) call_kwargs = mock_s3_client.put_object.call_args.kwargs csv_content = call_kwargs["Body"] lines = csv_content.strip().splitlines() assert len(lines) == 1 # header only assert lines[0] == "identity_id,tenant_type,tenant_uuid,error_reason"