diff --git a/integrated_channels/integrated_channel/transmitters/content_metadata.py b/integrated_channels/integrated_channel/transmitters/content_metadata.py index 49bb9a5d01..b3c704753b 100644 --- a/integrated_channels/integrated_channel/transmitters/content_metadata.py +++ b/integrated_channels/integrated_channel/transmitters/content_metadata.py @@ -16,7 +16,7 @@ from integrated_channels.exceptions import ClientError from integrated_channels.integrated_channel.client import IntegratedChannelApiClient from integrated_channels.integrated_channel.transmitters import Transmitter -from integrated_channels.utils import chunks, generate_formatted_log +from integrated_channels.utils import chunks, encode_binary_data_for_logging, generate_formatted_log LOGGER = logging.getLogger(__name__) @@ -153,6 +153,20 @@ def _transmit_action(self, content_metadata_item_map, client_method, action_name for chunk in islice(chunk_items, transmission_limit): json_payloads = [item.channel_metadata for item in list(chunk.values())] serialized_chunk = self._serialize_items(json_payloads) + if self.enterprise_configuration.dry_run_mode_enabled: + encoded_serialized_payload4 = encode_binary_data_for_logging(serialized_chunk) + enterprise_customer_uuid = self.enterprise_configuration.enterprise_customer.uuid + LOGGER.info(generate_formatted_log( + self.enterprise_configuration.channel_code(), + enterprise_customer_uuid, + None, + None, + f'dry-run mode content metadata ' + f'skipping "{action_name}" action for content metadata transmission ' + f'integrated_channel_serialized_payload_base64={encoded_serialized_payload4}' + )) + continue + response_status_code = None response_body = None try: diff --git a/integrated_channels/utils.py b/integrated_channels/utils.py index 9f1c3b17b9..b41c4b55f9 100644 --- a/integrated_channels/utils.py +++ b/integrated_channels/utils.py @@ -42,6 +42,21 @@ def encode_data_for_logging(data): return base64.urlsafe_b64encode(data.encode("utf-8")).decode('utf-8') +def encode_binary_data_for_logging(data): + """ + Converts binary input into URL-safe, utf-8 encoded, base64 encoded output. + If the input is binary (bytes), it is first decoded to utf-8, then dumped to JSON, + and finally, base64 encoded. + """ + if not isinstance(data, str): + try: + data = json.dumps(data.decode('utf-8')) + except (UnicodeDecodeError, AttributeError): + # Handle decoding errors or attribute errors (e.g., if 'data' is not bytes) + data = json.dumps(data) + return base64.urlsafe_b64encode(data.encode("utf-8")).decode('utf-8') + + def parse_datetime_to_epoch(datestamp, magnitude=1.0): """ Convert an ISO-8601 datetime string to a Unix epoch timestamp in some magnitude. diff --git a/tests/test_integrated_channels/test_integrated_channel/test_transmitters/test_content_metadata.py b/tests/test_integrated_channels/test_integrated_channel/test_transmitters/test_content_metadata.py index a60b523d0e..2db7d6d25d 100644 --- a/tests/test_integrated_channels/test_integrated_channel/test_transmitters/test_content_metadata.py +++ b/tests/test_integrated_channels/test_integrated_channel/test_transmitters/test_content_metadata.py @@ -490,3 +490,34 @@ def test_transmit_success_resolve_orphaned_content(self): orphaned_content_record.refresh_from_db() assert orphaned_content_record.resolved + + def test_content_data_transmission_dry_run_mode(self): + """ + Test that a customer's configuration can run in dry run mode + """ + # Set feature flag to true + self.enterprise_config.dry_run_mode_enabled = True + + content_id_1 = 'course:DemoX' + channel_metadata_1 = {'update': True} + content_1 = factories.ContentMetadataItemTransmissionFactory( + content_id=content_id_1, + enterprise_customer=self.enterprise_config.enterprise_customer, + plugin_configuration_id=self.enterprise_config.id, + integrated_channel_code=self.enterprise_config.channel_code(), + enterprise_customer_catalog_uuid=self.enterprise_catalog.uuid, + channel_metadata=channel_metadata_1, + remote_created_at=datetime.utcnow() + ) + + create_payload = {} + update_payload = {} + delete_payload = {content_id_1: content_1} + self.delete_content_metadata_mock.return_value = (self.success_response_code, self.success_response_body) + transmitter = ContentMetadataTransmitter(self.enterprise_config) + transmitter.transmit(create_payload, update_payload, delete_payload) + + # with dry_run_mode_enabled = True we shouldn't be able to call these methods + self.create_content_metadata_mock.assert_not_called() + self.update_content_metadata_mock.assert_not_called() + self.delete_content_metadata_mock.assert_not_called()