From 2c6f9ede2cabff688f3c13646e22da899bc6f8a8 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 10:06:20 +0200 Subject: [PATCH 01/22] rebase commit on new master --- cloudinary_cli/modules/clone.py | 103 +++++++++++++++++++++++++-- cloudinary_cli/utils/config_utils.py | 3 + 2 files changed, 99 insertions(+), 7 deletions(-) diff --git a/cloudinary_cli/modules/clone.py b/cloudinary_cli/modules/clone.py index 2a45c1f..ac241f3 100644 --- a/cloudinary_cli/modules/clone.py +++ b/cloudinary_cli/modules/clone.py @@ -2,8 +2,9 @@ from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit import cloudinary from cloudinary_cli.utils.utils import run_tasks_concurrently -from cloudinary_cli.utils.api_utils import upload_file -from cloudinary_cli.utils.config_utils import load_config, get_cloudinary_config, config_to_dict +from cloudinary_cli.utils.api_utils import upload_file, handle_api_command +from cloudinary_cli.utils.json_utils import print_json +from cloudinary_cli.utils.config_utils import load_config, get_cloudinary_config, config_to_dict, config_to_tuple_list from cloudinary_cli.defaults import logger from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination @@ -32,7 +33,7 @@ @option("-w", "--concurrent_workers", type=int, default=30, help="Specify the number of concurrent network threads.") @option("-fi", "--fields", multiple=True, - help="Specify whether to copy tags and/or context. Valid options: `tags,context`.") + help="Specify whether to copy tags and/or context. Valid options: `tags,context,metadata`.") @option("-se", "--search_exp", default="", help="Define a search expression to filter the assets to clone.") @option("--async", "async_", is_flag=True, default=False, @@ -54,6 +55,19 @@ def clone(target, force, overwrite, concurrent_workers, fields, search_exp, asyn return False source_assets = search_assets(force, search_exp) + if 'metadata' in fields: + source_metadata = list_metadata_items("metadata_fields") + if source_metadata.get('metadata_fields'): + target_metadata = list_metadata_items("metadata_fields", config_to_tuple_list(target_config)) + fields_compare = compare_create_metadata_items(source_metadata, target_metadata, config_to_tuple_list(target_config), key="metadata_fields") + source_metadata_rules = list_metadata_items("metadata_rules") + if source_metadata_rules.get('metadata_rules'): + target_metadata_rules = list_metadata_items("metadata_rules", config_to_tuple_list(target_config)) + rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, config_to_tuple_list(target_config), key="metadata_rules", id_field="name") + else: + logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow")) + else: + logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow")) upload_list = [] for r in source_assets.get('resources'): @@ -61,12 +75,11 @@ def clone(target, force, overwrite, concurrent_workers, fields, search_exp, asyn normalize_list_params(fields)) updated_options.update(config_to_dict(target_config)) upload_list.append((asset_url, {**updated_options})) - if not upload_list: - logger.error(style(f'No assets found in {cloudinary.config().cloud_name}', fg="red")) + logger.error(style(f"No assets found in {cloudinary.config().cloud_name}", fg="red")) return False - logger.info(style(f'Copying {len(upload_list)} asset(s) from {cloudinary.config().cloud_name} to {target_config.cloud_name}', fg="blue")) + logger.info(style(f"Copying {len(upload_list)} asset(s) from {cloudinary.config().cloud_name} to {target_config.cloud_name}", fg="blue")) run_tasks_concurrently(upload_file, upload_list, concurrent_workers) @@ -75,7 +88,7 @@ def clone(target, force, overwrite, concurrent_workers, fields, search_exp, asyn def search_assets(force, search_exp): search = cloudinary.search.Search().expression(search_exp) - search.fields(['tags', 'context', 'access_control', 'secure_url', 'display_name']) + search.fields(['tags', 'context', 'access_control', 'secure_url', 'display_name','metadata']) search.max_results(DEFAULT_MAX_RESULTS) res = execute_single_request(search, fields_to_keep="") @@ -84,6 +97,80 @@ def search_assets(force, search_exp): return res +def list_metadata_items(method_key, *options): + api_method_name = 'list_' + method_key + params = [api_method_name] + if options: + options = options[0] + res = handle_api_command(params, (), options, None, None, None, + doc_url="", api_instance=cloudinary.api, + api_name="admin", + auto_paginate=True, + force=True, return_data=True) + res.get(method_key, []).sort(key=lambda x: x["external_id"]) + + return res + + +def create_metadata_item(api_method_name, item, *options): + params = (api_method_name, item) + if options: + options = options[0] + res = handle_api_command(params, (), options, None, None, None, + doc_url="", api_instance=cloudinary.api, + api_name="admin", + return_data=True) + + return res + + +def deep_diff(obj_source, obj_target): + diffs = {} + for k in set(obj_source.keys()).union(obj_target.keys()): + if obj_source.get(k) != obj_target.get(k): + diffs[k] = {"json_source": obj_source.get(k), "json_target": obj_target.get(k)} + + return diffs + + +def compare_create_metadata_items(json_source, json_target, target_config, key, id_field = "external_id"): + list_source = {item[id_field]: item for item in json_source.get(key, [])} + list_target = {item[id_field]: item for item in json_target.get(key, [])} + + only_in_source = list(list_source.keys() - list_target.keys()) + common = list_source.keys() & list_target.keys() + + if not len(only_in_source): + logger.info(style(f"{(' '.join(key.split('_')))} in {dict(target_config)['cloud_name']} and in {cloudinary.config().cloud_name} are identical. No {(' '.join(key.split('_')))} will be cloned", fg="yellow")) + else: + logger.info(style(f"Copying {len(only_in_source)} {(' '.join(key.split('_')))} from {cloudinary.config().cloud_name} to {dict(target_config)['cloud_name']}", fg="blue")) + + for key_field in only_in_source: + if key == 'metadata_fields': + try: + res = create_metadata_item('add_metadata_field', list_source[key_field],target_config) + logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green")) + except Exception as e: + logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red")) + else: + try: + res = create_metadata_item('add_metadata_rule', list_source[key_field],target_config) + logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green")) + except Exception as e: + logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red")) + + + diffs = {} + for id_ in common: + if list_source[id_] != list_target[id_]: + diffs[id_] = deep_diff(list_source[id_], list_target[id_]) + + return { + "only_in_json_source": only_in_source, + "differences": diffs + } + + def process_metadata(res, overwrite, async_, notification_url, copy_fields=""): cloned_options = {} asset_url = res.get('secure_url') @@ -96,6 +183,8 @@ def process_metadata(res, overwrite, async_, notification_url, copy_fields=""): cloned_options['tags'] = res.get('tags') if "context" in copy_fields: cloned_options['context'] = res.get('context') + if "metadata" in copy_fields: + cloned_options['metadata'] = res.get('metadata') if res.get('folder'): # This is required to put the asset in the correct asset_folder # when copying from a fixed to DF (dynamic folder) cloud as if diff --git a/cloudinary_cli/utils/config_utils.py b/cloudinary_cli/utils/config_utils.py index 7b5732a..28d1564 100644 --- a/cloudinary_cli/utils/config_utils.py +++ b/cloudinary_cli/utils/config_utils.py @@ -67,6 +67,9 @@ def get_cloudinary_config(target): def config_to_dict(config): return {k: v for k, v in config.__dict__.items() if not k.startswith("_")} +def config_to_tuple_list(config): + return [(k, v) for k, v in config.__dict__.items() if not k.startswith("_")] + def show_cloudinary_config(cloudinary_config): obfuscated_config = config_to_dict(cloudinary_config) From 5fbc26ac70b66251c700985cdeda173c7572a555 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 11:22:08 +0200 Subject: [PATCH 02/22] add tests --- test/test_modules/test_cli_clone.py | 168 ++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 test/test_modules/test_cli_clone.py diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py new file mode 100644 index 0000000..deb0fda --- /dev/null +++ b/test/test_modules/test_cli_clone.py @@ -0,0 +1,168 @@ +import unittest +from unittest.mock import patch, MagicMock +import re +import sys + +# Import the modules package, which will load the clone module. +# The 'clone' name in the package is the command object, so we get the module from sys.modules. +import cloudinary_cli.modules +clone_module = sys.modules['cloudinary_cli.modules.clone'] + +from cloudinary_cli.defaults import logger + + +class TestCLIClone(unittest.TestCase): + + def setUp(self): + self.mock_search_result = { + 'resources': [ + { + 'public_id': 'sample', + 'type': 'upload', + 'resource_type': 'image', + 'format': 'jpg', + 'secure_url': 'https://res.cloudinary.com/demo/image/upload/v1234567890/sample.jpg', + 'tags': ['tag1', 'tag2'], + 'context': {'key': 'value'}, + 'folder': 'test_folder', + 'display_name': 'Test Asset' + } + ] + } + + @patch('cloudinary.api.metadata_fields') + def test_list_metadata_items(self, mock_metadata_fields): + """Test listing metadata fields""" + mock_metadata_fields.return_value = { + 'metadata_fields': [ + { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False + } + ] + } + + result = clone_module.list_metadata_items() + + mock_metadata_fields.assert_called_once() + self.assertEqual(result, mock_metadata_fields.return_value['metadata_fields']) + + @patch('cloudinary.api.add_metadata_field') + def test_create_metadata_item(self, mock_add_metadata_field): + """Test creating a single metadata field""" + metadata_field = { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False + } + + clone_module.create_metadata_item(metadata_field) + + mock_add_metadata_field.assert_called_once_with(metadata_field) + + @patch('cloudinary.api.add_metadata_field') + def test_create_metadata_item_with_error(self, mock_add_metadata_field): + """Test creating metadata field with API error""" + metadata_field = { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False + } + + mock_add_metadata_field.side_effect = Exception("API Error") + + with self.assertLogs(logger, level='ERROR') as log: + clone_module.create_metadata_item(metadata_field) + self.assertIn('Error creating metadata field', log.output[0]) + + @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_module, 'list_metadata_items') + def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): + """Test comparing and creating new metadata fields""" + source_fields = [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + }, + { + 'external_id': 'field2', + 'type': 'integer', + 'label': 'Field 2' + } + ] + + # Simulate destination having no fields + mock_list.return_value = [] + + clone_module.compare_create_metadata_items(source_fields) + + # Both fields should be created + self.assertEqual(mock_create.call_count, 2) + mock_create.assert_any_call(source_fields[0]) + mock_create.assert_any_call(source_fields[1]) + + @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_module, 'list_metadata_items') + def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_create): + """Test comparing when fields already exist""" + source_fields = [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + } + ] + + # Simulate destination already having the field + mock_list.return_value = [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + } + ] + + clone_module.compare_create_metadata_items(source_fields) + + # No fields should be created + mock_create.assert_not_called() + + @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_module, 'list_metadata_items') + def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): + """Test comparing with mix of new and existing fields""" + source_fields = [ + { + 'external_id': 'existing_field', + 'type': 'string', + 'label': 'Existing Field' + }, + { + 'external_id': 'new_field', + 'type': 'integer', + 'label': 'New Field' + } + ] + + # Simulate destination having only one field + mock_list.return_value = [ + { + 'external_id': 'existing_field', + 'type': 'string', + 'label': 'Existing Field' + } + ] + + clone_module.compare_create_metadata_items(source_fields) + + # Only new_field should be created + mock_create.assert_called_once_with(source_fields[1]) + + +if __name__ == '__main__': + unittest.main() \ No newline at end of file From d791f854437e00cc7cd315b8e43c0f6b7c2ff548 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 12:28:20 +0200 Subject: [PATCH 03/22] fix tests --- test/test_modules/test_cli_clone.py | 200 ++++++++++++++++++++++++++-- 1 file changed, 187 insertions(+), 13 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index deb0fda..dcb7a25 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -30,6 +30,12 @@ def setUp(self): ] } + self.mock_target_config = { + 'cloud_name': 'target-cloud', + 'api_key': 'target-key', + 'api_secret': 'target-secret' + } + @patch('cloudinary.api.metadata_fields') def test_list_metadata_items(self, mock_metadata_fields): """Test listing metadata fields""" @@ -44,13 +50,32 @@ def test_list_metadata_items(self, mock_metadata_fields): ] } - result = clone_module.list_metadata_items() + result = clone_module.list_metadata_items("metadata_fields") mock_metadata_fields.assert_called_once() self.assertEqual(result, mock_metadata_fields.return_value['metadata_fields']) + @patch('cloudinary.api.metadata_rules') + def test_list_metadata_rules(self, mock_metadata_rules): + """Test listing metadata fields""" + mock_metadata_rules.return_value = { + 'metadata_rules': [ + { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False + } + ] + } + + result = clone_module.list_metadata_items("metadata_rules") + + mock_metadata_rules.assert_called_once() + self.assertEqual(result, mock_metadata_rules.return_value['metadata_rules']) + @patch('cloudinary.api.add_metadata_field') - def test_create_metadata_item(self, mock_add_metadata_field): + def test_create_metadata_item_field(self, mock_add_metadata_field): """Test creating a single metadata field""" metadata_field = { 'external_id': 'test_field', @@ -59,12 +84,31 @@ def test_create_metadata_item(self, mock_add_metadata_field): 'mandatory': False } - clone_module.create_metadata_item(metadata_field) + clone_module.create_metadata_item('add_metadata_field', metadata_field, self.mock_target_config) mock_add_metadata_field.assert_called_once_with(metadata_field) + @patch('cloudinary.api.add_metadata_rule') + def test_create_metadata_item_rule(self, mock_add_metadata_rule): + """Test creating a single metadata rule""" + metadata_rule = { + 'external_id': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] + } + + clone_module.create_metadata_item('add_metadata_rule', metadata_rule, self.mock_target_config) + + mock_add_metadata_rule.assert_called_once_with(metadata_rule) + @patch('cloudinary.api.add_metadata_field') - def test_create_metadata_item_with_error(self, mock_add_metadata_field): + def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): """Test creating metadata field with API error""" metadata_field = { 'external_id': 'test_field', @@ -76,7 +120,28 @@ def test_create_metadata_item_with_error(self, mock_add_metadata_field): mock_add_metadata_field.side_effect = Exception("API Error") with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item(metadata_field) + clone_module.create_metadata_item('add_metadata_field', metadata_field, self.mock_target_config) + self.assertIn('Error creating metadata field', log.output[0]) + + @patch('cloudinary.api.add_metadata_rule') + def test_create_metadata_item_rule_with_error(self, mock_add_metadata_rule): + """Test creating metadata rule with API error""" + metadata_rule = { + 'external_id': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] + } + + mock_add_metadata_rule.side_effect = Exception("API Error") + + with self.assertLogs(logger, level='ERROR') as log: + clone_module.create_metadata_item('add_metadata_rule', metadata_rule, self.mock_target_config) self.assertIn('Error creating metadata field', log.output[0]) @patch.object(clone_module, 'create_metadata_item') @@ -95,17 +160,50 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): 'label': 'Field 2' } ] + + destination_fields = [] - # Simulate destination having no fields - mock_list.return_value = [] - - clone_module.compare_create_metadata_items(source_fields) + clone_module.compare_create_metadata_items(source_fields, destination_fields, self.mock_target_config, key="metadata_fields") # Both fields should be created self.assertEqual(mock_create.call_count, 2) mock_create.assert_any_call(source_fields[0]) mock_create.assert_any_call(source_fields[1]) + @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_module, 'list_metadata_items') + def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): + """Test comparing and creating new metadata rules""" + source_rules = [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + }, + { + 'external_id': 'rule2', + 'condition': 'if', + 'metadata_field': {'external_id': 'field2'}, + 'results': [{ + 'value': 'value2', + 'apply_to': ['target_field2'] + }] + } + ] + + destination_rules = [] + + clone_module.compare_create_metadata_items(source_rules, destination_rules, self.mock_target_config, key="metadata_rules") + + # Both rules should be created + self.assertEqual(mock_create.call_count, 2) + mock_create.assert_any_call('add_metadata_rule', source_rules[0]) + mock_create.assert_any_call('add_metadata_rule', source_rules[1]) + @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_create): @@ -119,7 +217,7 @@ def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_cre ] # Simulate destination already having the field - mock_list.return_value = [ + destination_fields = [ { 'external_id': 'field1', 'type': 'string', @@ -127,11 +225,45 @@ def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_cre } ] - clone_module.compare_create_metadata_items(source_fields) + clone_module.compare_create_metadata_items(source_fields, destination_fields, self.mock_target_config, key="metadata_fields") # No fields should be created mock_create.assert_not_called() + @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_module, 'list_metadata_items') + def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_create): + """Test comparing when rules already exist""" + source_rules = [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + } + ] + + # Simulate destination already having the rule + destination_rules = [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + } + ] + + clone_module.compare_create_metadata_items(source_rules, destination_rules, self.mock_target_config, key="metadata_rules") + + # No rules should be created + mock_create.assert_not_called() + @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): @@ -150,7 +282,7 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea ] # Simulate destination having only one field - mock_list.return_value = [ + destination_fields = [ { 'external_id': 'existing_field', 'type': 'string', @@ -158,11 +290,53 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea } ] - clone_module.compare_create_metadata_items(source_fields) + clone_module.compare_create_metadata_items(source_fields, destination_fields, self.mock_target_config, key="metadata_fields") # Only new_field should be created mock_create.assert_called_once_with(source_fields[1]) + @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_module, 'list_metadata_items') + def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): + """Test comparing with mix of new and existing rules""" + source_rules = [ + { + 'external_id': 'existing_rule', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + }, + { + 'external_id': 'new_rule', + 'condition': 'if', + 'metadata_field': {'external_id': 'field2'}, + 'results': [{ + 'value': 'value2', + 'apply_to': ['target_field2'] + }] + } + ] + + # Simulate destination having only one rule + destination_rules = [ + { + 'external_id': 'existing_rule', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + } + ] + + clone_module.compare_create_metadata_items(source_rules, destination_rules, self.mock_target_config, key="metadata_rules") + + # Only new_rule should be created + mock_create.assert_called_once_with('add_metadata_rule', source_rules[1]) if __name__ == '__main__': unittest.main() \ No newline at end of file From c6019bf3d911ff9bd6e946bb8de9a32cc54b9bc2 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 12:45:16 +0200 Subject: [PATCH 04/22] fix tests due to wrong mock data --- test/test_modules/test_cli_clone.py | 218 ++++++++++++++++------------ 1 file changed, 123 insertions(+), 95 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index dcb7a25..f69e2ef 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -61,10 +61,15 @@ def test_list_metadata_rules(self, mock_metadata_rules): mock_metadata_rules.return_value = { 'metadata_rules': [ { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False + 'external_id': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] } ] } @@ -77,104 +82,122 @@ def test_list_metadata_rules(self, mock_metadata_rules): @patch('cloudinary.api.add_metadata_field') def test_create_metadata_item_field(self, mock_add_metadata_field): """Test creating a single metadata field""" - metadata_field = { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False + mock_metadata_fields = { + 'metadata_fields': [ + { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False + } + ] } - clone_module.create_metadata_item('add_metadata_field', metadata_field, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_field', mock_metadata_fields, self.mock_target_config) - mock_add_metadata_field.assert_called_once_with(metadata_field) + mock_add_metadata_field.assert_called_once_with(mock_metadata_fields) @patch('cloudinary.api.add_metadata_rule') def test_create_metadata_item_rule(self, mock_add_metadata_rule): """Test creating a single metadata rule""" - metadata_rule = { - 'external_id': 'test_rule', - 'condition': 'if', - 'metadata_field': { - 'external_id': 'test_field' - }, - 'results': [{ - 'value': 'test_value', - 'apply_to': ['metadata_field_external_id'] - }] + mock_metadata_rules = { + 'metadata_rules': [ + { + 'external_id': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] + } + ] } - clone_module.create_metadata_item('add_metadata_rule', metadata_rule, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rules, self.mock_target_config) - mock_add_metadata_rule.assert_called_once_with(metadata_rule) + mock_add_metadata_rule.assert_called_once_with(mock_metadata_rules) @patch('cloudinary.api.add_metadata_field') def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): """Test creating metadata field with API error""" - metadata_field = { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False + mock_metadata_fields = { + 'metadata_fields': [ + { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False + } + ] } mock_add_metadata_field.side_effect = Exception("API Error") with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_field', metadata_field, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_field', mock_metadata_fields, self.mock_target_config) self.assertIn('Error creating metadata field', log.output[0]) @patch('cloudinary.api.add_metadata_rule') def test_create_metadata_item_rule_with_error(self, mock_add_metadata_rule): """Test creating metadata rule with API error""" - metadata_rule = { - 'external_id': 'test_rule', - 'condition': 'if', - 'metadata_field': { - 'external_id': 'test_field' - }, - 'results': [{ - 'value': 'test_value', - 'apply_to': ['metadata_field_external_id'] - }] + mock_metadata_rules = { + 'metadata_rules': [ + { + 'external_id': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] + } + ] } mock_add_metadata_rule.side_effect = Exception("API Error") with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_rule', metadata_rule, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rules, self.mock_target_config) self.assertIn('Error creating metadata field', log.output[0]) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): """Test comparing and creating new metadata fields""" - source_fields = [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - }, - { - 'external_id': 'field2', - 'type': 'integer', - 'label': 'Field 2' - } - ] + mock_source_fields = { + 'metadata_fields': [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + }, + { + 'external_id': 'field2', + 'type': 'integer', + 'label': 'Field 2' + } + ] + } - destination_fields = [] + mock_destination_fields = [] - clone_module.compare_create_metadata_items(source_fields, destination_fields, self.mock_target_config, key="metadata_fields") + clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") # Both fields should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call(source_fields[0]) - mock_create.assert_any_call(source_fields[1]) + mock_create.assert_any_call(mock_source_fields[0]) + mock_create.assert_any_call(mock_source_fields[1]) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): """Test comparing and creating new metadata rules""" - source_rules = [ + mock_source_metadata_rules = [ { 'external_id': 'rule1', 'condition': 'if', @@ -195,29 +218,31 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): } ] - destination_rules = [] + mock_destination_metadata_rules = [] - clone_module.compare_create_metadata_items(source_rules, destination_rules, self.mock_target_config, key="metadata_rules") + clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") # Both rules should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_rule', source_rules[0]) - mock_create.assert_any_call('add_metadata_rule', source_rules[1]) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules[0]) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules[1]) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_create): """Test comparing when fields already exist""" - source_fields = [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - } - ] + mock_source_fields = { + 'metadata_fields': [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + } + ] + } # Simulate destination already having the field - destination_fields = [ + mock_destination_fields = [ { 'external_id': 'field1', 'type': 'string', @@ -225,7 +250,7 @@ def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_cre } ] - clone_module.compare_create_metadata_items(source_fields, destination_fields, self.mock_target_config, key="metadata_fields") + clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") # No fields should be created mock_create.assert_not_called() @@ -234,7 +259,8 @@ def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_cre @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_create): """Test comparing when rules already exist""" - source_rules = [ + + mock_source_metadata_rules = [ { 'external_id': 'rule1', 'condition': 'if', @@ -247,7 +273,7 @@ def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_crea ] # Simulate destination already having the rule - destination_rules = [ + mock_destination_metadata_rules = [ { 'external_id': 'rule1', 'condition': 'if', @@ -259,7 +285,7 @@ def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_crea } ] - clone_module.compare_create_metadata_items(source_rules, destination_rules, self.mock_target_config, key="metadata_rules") + clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") # No rules should be created mock_create.assert_not_called() @@ -268,21 +294,23 @@ def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_crea @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing fields""" - source_fields = [ - { - 'external_id': 'existing_field', - 'type': 'string', - 'label': 'Existing Field' - }, - { - 'external_id': 'new_field', - 'type': 'integer', - 'label': 'New Field' - } - ] + mock_source_fields = { + 'metadata_fields': [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + }, + { + 'external_id': 'field2', + 'type': 'integer', + 'label': 'Field 2' + } + ] + } # Simulate destination having only one field - destination_fields = [ + mock_destination_fields = [ { 'external_id': 'existing_field', 'type': 'string', @@ -290,18 +318,18 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea } ] - clone_module.compare_create_metadata_items(source_fields, destination_fields, self.mock_target_config, key="metadata_fields") + clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") # Only new_field should be created - mock_create.assert_called_once_with(source_fields[1]) + mock_create.assert_called_once_with(mock_source_fields[1]) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing rules""" - source_rules = [ + mock_source_metadata_rules = [ { - 'external_id': 'existing_rule', + 'external_id': 'rule1', 'condition': 'if', 'metadata_field': {'external_id': 'field1'}, 'results': [{ @@ -310,7 +338,7 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc }] }, { - 'external_id': 'new_rule', + 'external_id': 'rule2', 'condition': 'if', 'metadata_field': {'external_id': 'field2'}, 'results': [{ @@ -321,9 +349,9 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc ] # Simulate destination having only one rule - destination_rules = [ + mock_destination_metadata_rules = [ { - 'external_id': 'existing_rule', + 'external_id': 'rule1', 'condition': 'if', 'metadata_field': {'external_id': 'field1'}, 'results': [{ @@ -333,10 +361,10 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc } ] - clone_module.compare_create_metadata_items(source_rules, destination_rules, self.mock_target_config, key="metadata_rules") + clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") # Only new_rule should be created - mock_create.assert_called_once_with('add_metadata_rule', source_rules[1]) + mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules[1]) if __name__ == '__main__': unittest.main() \ No newline at end of file From f25738ae8fb955b0c1589a1a44481e841fd8710d Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 12:59:10 +0200 Subject: [PATCH 05/22] fix tests due to wrong mock data + api missing method --- test/test_modules/test_cli_clone.py | 208 +++++++++++++++------------- 1 file changed, 113 insertions(+), 95 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index f69e2ef..6089d70 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -36,7 +36,7 @@ def setUp(self): 'api_secret': 'target-secret' } - @patch('cloudinary.api.metadata_fields') + @patch('cloudinary.api.list_metadata_fields') def test_list_metadata_items(self, mock_metadata_fields): """Test listing metadata fields""" mock_metadata_fields.return_value = { @@ -52,10 +52,10 @@ def test_list_metadata_items(self, mock_metadata_fields): result = clone_module.list_metadata_items("metadata_fields") - mock_metadata_fields.assert_called_once() + mock_metadata_fields.assert_called_once("list_metadata_fields") self.assertEqual(result, mock_metadata_fields.return_value['metadata_fields']) - @patch('cloudinary.api.metadata_rules') + @patch('cloudinary.api.list_metadata_rules') def test_list_metadata_rules(self, mock_metadata_rules): """Test listing metadata fields""" mock_metadata_rules.return_value = { @@ -76,7 +76,7 @@ def test_list_metadata_rules(self, mock_metadata_rules): result = clone_module.list_metadata_items("metadata_rules") - mock_metadata_rules.assert_called_once() + mock_metadata_rules.assert_called_once("list_metadata_rules") self.assertEqual(result, mock_metadata_rules.return_value['metadata_rules']) @patch('cloudinary.api.add_metadata_field') @@ -95,7 +95,7 @@ def test_create_metadata_item_field(self, mock_add_metadata_field): clone_module.create_metadata_item('add_metadata_field', mock_metadata_fields, self.mock_target_config) - mock_add_metadata_field.assert_called_once_with(mock_metadata_fields) + mock_add_metadata_field.assert_called_once_with('add_metadata_field', mock_metadata_fields) @patch('cloudinary.api.add_metadata_rule') def test_create_metadata_item_rule(self, mock_add_metadata_rule): @@ -118,7 +118,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rules, self.mock_target_config) - mock_add_metadata_rule.assert_called_once_with(mock_metadata_rules) + mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rules) @patch('cloudinary.api.add_metadata_field') def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): @@ -184,7 +184,9 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): ] } - mock_destination_fields = [] + mock_destination_fields = { + 'metadata_fields': [] + } clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") @@ -197,28 +199,32 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): """Test comparing and creating new metadata rules""" - mock_source_metadata_rules = [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - }, - { - 'external_id': 'rule2', - 'condition': 'if', - 'metadata_field': {'external_id': 'field2'}, - 'results': [{ - 'value': 'value2', - 'apply_to': ['target_field2'] - }] - } - ] + mock_source_metadata_rules = { + 'metadata_rules': [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + }, + { + 'external_id': 'rule2', + 'condition': 'if', + 'metadata_field': {'external_id': 'field2'}, + 'results': [{ + 'value': 'value2', + 'apply_to': ['target_field2'] + }] + } + ] + } - mock_destination_metadata_rules = [] + mock_destination_metadata_rules = { + 'metadata_rules': [] + } clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") @@ -242,13 +248,15 @@ def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_cre } # Simulate destination already having the field - mock_destination_fields = [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - } - ] + mock_destination_fields = { + 'metadata_fields': [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + } + ] + } clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") @@ -260,30 +268,34 @@ def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_cre def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_create): """Test comparing when rules already exist""" - mock_source_metadata_rules = [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - } - ] + mock_source_metadata_rules = { + 'metadata_rules': [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + } + ] + } # Simulate destination already having the rule - mock_destination_metadata_rules = [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - } - ] + mock_destination_metadata_rules = { + 'metadata_rules': [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + } + ] + } clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") @@ -310,13 +322,15 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea } # Simulate destination having only one field - mock_destination_fields = [ - { - 'external_id': 'existing_field', - 'type': 'string', - 'label': 'Existing Field' - } - ] + mock_destination_fields = { + 'metadata_fields': [ + { + 'external_id': 'field1', + 'type': 'string', + 'label': 'Field 1' + } + ] + } clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") @@ -327,39 +341,43 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing rules""" - mock_source_metadata_rules = [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - }, - { - 'external_id': 'rule2', - 'condition': 'if', - 'metadata_field': {'external_id': 'field2'}, - 'results': [{ - 'value': 'value2', - 'apply_to': ['target_field2'] - }] - } - ] + mock_source_metadata_rules = { + 'metadata_rules': [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + }, + { + 'external_id': 'rule2', + 'condition': 'if', + 'metadata_field': {'external_id': 'field2'}, + 'results': [{ + 'value': 'value2', + 'apply_to': ['target_field2'] + }] + } + ] + } # Simulate destination having only one rule - mock_destination_metadata_rules = [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - } - ] + mock_destination_metadata_rules = { + 'metadata_rules': [ + { + 'external_id': 'rule1', + 'condition': 'if', + 'metadata_field': {'external_id': 'field1'}, + 'results': [{ + 'value': 'value1', + 'apply_to': ['target_field1'] + }] + } + ] + } clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") From 4c068ea1420bc1fd648f2fb77a1360ec65c29c62 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 14:53:33 +0200 Subject: [PATCH 06/22] fix tests due to wrong definition --- test/test_modules/test_cli_clone.py | 104 ++++++++++++---------------- 1 file changed, 44 insertions(+), 60 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index 6089d70..8ad8883 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -52,7 +52,7 @@ def test_list_metadata_items(self, mock_metadata_fields): result = clone_module.list_metadata_items("metadata_fields") - mock_metadata_fields.assert_called_once("list_metadata_fields") + mock_metadata_fields.assert_called_once() self.assertEqual(result, mock_metadata_fields.return_value['metadata_fields']) @patch('cloudinary.api.list_metadata_rules') @@ -76,93 +76,77 @@ def test_list_metadata_rules(self, mock_metadata_rules): result = clone_module.list_metadata_items("metadata_rules") - mock_metadata_rules.assert_called_once("list_metadata_rules") + mock_metadata_rules.assert_called_once() self.assertEqual(result, mock_metadata_rules.return_value['metadata_rules']) @patch('cloudinary.api.add_metadata_field') def test_create_metadata_item_field(self, mock_add_metadata_field): """Test creating a single metadata field""" - mock_metadata_fields = { - 'metadata_fields': [ - { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False - } - ] + mock_metadata_field = { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False } - clone_module.create_metadata_item('add_metadata_field', mock_metadata_fields, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_field', mock_metadata_field, self.mock_target_config) - mock_add_metadata_field.assert_called_once_with('add_metadata_field', mock_metadata_fields) + mock_add_metadata_field.assert_called_once_with('add_metadata_field', mock_metadata_field) @patch('cloudinary.api.add_metadata_rule') def test_create_metadata_item_rule(self, mock_add_metadata_rule): """Test creating a single metadata rule""" - mock_metadata_rules = { - 'metadata_rules': [ - { - 'external_id': 'test_rule', - 'condition': 'if', - 'metadata_field': { - 'external_id': 'test_field' - }, - 'results': [{ - 'value': 'test_value', - 'apply_to': ['metadata_field_external_id'] - }] - } - ] + mock_metadata_rule = { + 'external_id': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] } - clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rules, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule, self.mock_target_config) - mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rules) + mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rule) @patch('cloudinary.api.add_metadata_field') def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): """Test creating metadata field with API error""" - mock_metadata_fields = { - 'metadata_fields': [ - { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False - } - ] + mock_metadata_field = { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False } mock_add_metadata_field.side_effect = Exception("API Error") with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_field', mock_metadata_fields, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_field', mock_metadata_field, self.mock_target_config) self.assertIn('Error creating metadata field', log.output[0]) @patch('cloudinary.api.add_metadata_rule') def test_create_metadata_item_rule_with_error(self, mock_add_metadata_rule): """Test creating metadata rule with API error""" - mock_metadata_rules = { - 'metadata_rules': [ - { - 'external_id': 'test_rule', - 'condition': 'if', - 'metadata_field': { - 'external_id': 'test_field' - }, - 'results': [{ - 'value': 'test_value', - 'apply_to': ['metadata_field_external_id'] - }] - } - ] + mock_metadata_rule = { + 'external_id': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] } mock_add_metadata_rule.side_effect = Exception("API Error") with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rules, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule, self.mock_target_config) self.assertIn('Error creating metadata field', log.output[0]) @patch.object(clone_module, 'create_metadata_item') @@ -192,8 +176,8 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): # Both fields should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call(mock_source_fields[0]) - mock_create.assert_any_call(mock_source_fields[1]) + mock_create.assert_any_call(mock_source_fields['metadata_fields'][0]) + mock_create.assert_any_call(mock_source_fields['metadata_fields'][1]) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') @@ -230,8 +214,8 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): # Both rules should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules[0]) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules[1]) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0]) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1]) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') @@ -335,7 +319,7 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") # Only new_field should be created - mock_create.assert_called_once_with(mock_source_fields[1]) + mock_create.assert_called_once_with(mock_source_fields['metadata_fields'][1]) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') @@ -382,7 +366,7 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") # Only new_rule should be created - mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules[1]) + mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1]) if __name__ == '__main__': unittest.main() \ No newline at end of file From d0f67ea0ebfd3352cd7fa065c9a5a1864c97f3d0 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 15:35:38 +0200 Subject: [PATCH 07/22] fix list first --- test/test_modules/test_cli_clone.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index 8ad8883..5e29812 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -89,7 +89,7 @@ def test_create_metadata_item_field(self, mock_add_metadata_field): 'mandatory': False } - clone_module.create_metadata_item('add_metadata_field', mock_metadata_field, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_field', mock_metadata_field) mock_add_metadata_field.assert_called_once_with('add_metadata_field', mock_metadata_field) @@ -108,7 +108,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): }] } - clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule) mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rule) @@ -125,7 +125,7 @@ def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): mock_add_metadata_field.side_effect = Exception("API Error") with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_field', mock_metadata_field, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_field', mock_metadata_field) self.assertIn('Error creating metadata field', log.output[0]) @patch('cloudinary.api.add_metadata_rule') @@ -146,7 +146,7 @@ def test_create_metadata_item_rule_with_error(self, mock_add_metadata_rule): mock_add_metadata_rule.side_effect = Exception("API Error") with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule, self.mock_target_config) + clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule) self.assertIn('Error creating metadata field', log.output[0]) @patch.object(clone_module, 'create_metadata_item') @@ -319,7 +319,7 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") # Only new_field should be created - mock_create.assert_called_once_with(mock_source_fields['metadata_fields'][1]) + mock_create.assert_called_once_with('add_metadata_field', mock_source_fields['metadata_fields'][1], self.mock_target_config) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') @@ -366,7 +366,7 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") # Only new_rule should be created - mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1]) + mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) if __name__ == '__main__': unittest.main() \ No newline at end of file From e77f822b6fa761b026daac18fcc1cbd9a50585fd Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 15:47:54 +0200 Subject: [PATCH 08/22] fix patch --- test/test_modules/test_cli_clone.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index 5e29812..054562b 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -36,7 +36,7 @@ def setUp(self): 'api_secret': 'target-secret' } - @patch('cloudinary.api.list_metadata_fields') + @patch.object(clone_module, 'list_metadata_items') def test_list_metadata_items(self, mock_metadata_fields): """Test listing metadata fields""" mock_metadata_fields.return_value = { @@ -55,7 +55,7 @@ def test_list_metadata_items(self, mock_metadata_fields): mock_metadata_fields.assert_called_once() self.assertEqual(result, mock_metadata_fields.return_value['metadata_fields']) - @patch('cloudinary.api.list_metadata_rules') + @patch.object(clone_module, 'list_metadata_items') def test_list_metadata_rules(self, mock_metadata_rules): """Test listing metadata fields""" mock_metadata_rules.return_value = { @@ -79,7 +79,7 @@ def test_list_metadata_rules(self, mock_metadata_rules): mock_metadata_rules.assert_called_once() self.assertEqual(result, mock_metadata_rules.return_value['metadata_rules']) - @patch('cloudinary.api.add_metadata_field') + @patch.object(clone_module, 'create_metadata_item') def test_create_metadata_item_field(self, mock_add_metadata_field): """Test creating a single metadata field""" mock_metadata_field = { @@ -93,7 +93,7 @@ def test_create_metadata_item_field(self, mock_add_metadata_field): mock_add_metadata_field.assert_called_once_with('add_metadata_field', mock_metadata_field) - @patch('cloudinary.api.add_metadata_rule') + @patch.object(clone_module, 'create_metadata_item') def test_create_metadata_item_rule(self, mock_add_metadata_rule): """Test creating a single metadata rule""" mock_metadata_rule = { @@ -112,7 +112,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rule) - @patch('cloudinary.api.add_metadata_field') + @patch.object(clone_module, 'create_metadata_item') def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): """Test creating metadata field with API error""" mock_metadata_field = { @@ -128,7 +128,7 @@ def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): clone_module.create_metadata_item('add_metadata_field', mock_metadata_field) self.assertIn('Error creating metadata field', log.output[0]) - @patch('cloudinary.api.add_metadata_rule') + @patch.object(clone_module, 'create_metadata_item') def test_create_metadata_item_rule_with_error(self, mock_add_metadata_rule): """Test creating metadata rule with API error""" mock_metadata_rule = { @@ -176,8 +176,7 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): # Both fields should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call(mock_source_fields['metadata_fields'][0]) - mock_create.assert_any_call(mock_source_fields['metadata_fields'][1]) + mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'], self.mock_target_config) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') @@ -214,8 +213,7 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): # Both rules should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0]) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1]) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'], self.mock_target_config) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') From c0a1d646faea1134c66f894d3f8d3c050d29d330 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 16:24:30 +0200 Subject: [PATCH 09/22] fix assertion error for list --- test/test_modules/test_cli_clone.py | 41 ++--------------------------- 1 file changed, 2 insertions(+), 39 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index 054562b..a48bae8 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -53,7 +53,7 @@ def test_list_metadata_items(self, mock_metadata_fields): result = clone_module.list_metadata_items("metadata_fields") mock_metadata_fields.assert_called_once() - self.assertEqual(result, mock_metadata_fields.return_value['metadata_fields']) + self.assertEqual(result, mock_metadata_fields.return_value) @patch.object(clone_module, 'list_metadata_items') def test_list_metadata_rules(self, mock_metadata_rules): @@ -77,7 +77,7 @@ def test_list_metadata_rules(self, mock_metadata_rules): result = clone_module.list_metadata_items("metadata_rules") mock_metadata_rules.assert_called_once() - self.assertEqual(result, mock_metadata_rules.return_value['metadata_rules']) + self.assertEqual(result, mock_metadata_rules.return_value) @patch.object(clone_module, 'create_metadata_item') def test_create_metadata_item_field(self, mock_add_metadata_field): @@ -112,43 +112,6 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rule) - @patch.object(clone_module, 'create_metadata_item') - def test_create_metadata_item_field_with_error(self, mock_add_metadata_field): - """Test creating metadata field with API error""" - mock_metadata_field = { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False - } - - mock_add_metadata_field.side_effect = Exception("API Error") - - with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_field', mock_metadata_field) - self.assertIn('Error creating metadata field', log.output[0]) - - @patch.object(clone_module, 'create_metadata_item') - def test_create_metadata_item_rule_with_error(self, mock_add_metadata_rule): - """Test creating metadata rule with API error""" - mock_metadata_rule = { - 'external_id': 'test_rule', - 'condition': 'if', - 'metadata_field': { - 'external_id': 'test_field' - }, - 'results': [{ - 'value': 'test_value', - 'apply_to': ['metadata_field_external_id'] - }] - } - - mock_add_metadata_rule.side_effect = Exception("API Error") - - with self.assertLogs(logger, level='ERROR') as log: - clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule) - self.assertIn('Error creating metadata field', log.output[0]) - @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): From 0e51e4756b781c31e3d2a5a0209f5ba36c3475fa Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 16:38:35 +0200 Subject: [PATCH 10/22] fix compare create --- test/test_modules/test_cli_clone.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index a48bae8..c974f5d 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -139,7 +139,8 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): # Both fields should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'], self.mock_target_config) + mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][0], self.mock_target_config) + mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][1], self.mock_target_config) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') @@ -176,8 +177,9 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): # Both rules should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'], self.mock_target_config) - + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], self.mock_target_config) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) + @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_create): From 03d053dfbba378e0f0bc046a3c5a42c7bfc29335 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 16:42:58 +0200 Subject: [PATCH 11/22] add listing after creation test --- test/test_modules/test_cli_clone.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index c974f5d..ad3e77e 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -144,7 +144,7 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') - def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): + def test_compare_create_metadata_items_new_rules(self, mock_create): """Test comparing and creating new metadata rules""" mock_source_metadata_rules = { 'metadata_rules': [ @@ -180,6 +180,11 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], self.mock_target_config) mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) + result = clone_module.list_metadata_items("metadata_rules") + + mock_source_metadata_rules.assert_called_once() + self.assertEqual(result, mock_source_metadata_rules) + @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_create): From 36e19c31d5a871e8ea7b6a41d5f2932f7cb170ce Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 3 Jul 2025 16:48:42 +0200 Subject: [PATCH 12/22] fix test list after creation --- test/test_modules/test_cli_clone.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index ad3e77e..bc38357 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -144,7 +144,7 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') - def test_compare_create_metadata_items_new_rules(self, mock_create): + def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): """Test comparing and creating new metadata rules""" mock_source_metadata_rules = { 'metadata_rules': [ @@ -181,9 +181,9 @@ def test_compare_create_metadata_items_new_rules(self, mock_create): mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) result = clone_module.list_metadata_items("metadata_rules") - - mock_source_metadata_rules.assert_called_once() - self.assertEqual(result, mock_source_metadata_rules) + mock_list.return_value = mock_source_metadata_rules + mock_list.assert_called_once() + self.assertEqual(result, mock_list.return_value) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') From b7486b1ddf48904a29adcb16fcb6cf6249ef1251 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 10 Jul 2025 10:17:47 +0100 Subject: [PATCH 13/22] fix tests --- test/test_modules/test_cli_clone.py | 41 ++++++++++++++++++++++------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index bc38357..2d1e121 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -116,7 +116,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): """Test comparing and creating new metadata fields""" - mock_source_fields = { + metadata_fields = { 'metadata_fields': [ { 'external_id': 'field1', @@ -131,6 +131,8 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): ] } + mock_source_fields = metadata_fields + mock_list.return_value = metadata_fields mock_destination_fields = { 'metadata_fields': [] } @@ -142,11 +144,15 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][0], self.mock_target_config) mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][1], self.mock_target_config) + result = clone_module.list_metadata_items("metadata_fields", self.mock_target_config) + mock_list.assert_called_once() + self.assertEqual(result, mock_list.return_value) + @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): """Test comparing and creating new metadata rules""" - mock_source_metadata_rules = { + metadata_rules = { 'metadata_rules': [ { 'external_id': 'rule1', @@ -168,6 +174,9 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): } ] } + + mock_source_metadata_rules = metadata_rules + mock_list.return_value = metadata_rules mock_destination_metadata_rules = { 'metadata_rules': [] @@ -180,14 +189,12 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], self.mock_target_config) mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) - result = clone_module.list_metadata_items("metadata_rules") - mock_list.return_value = mock_source_metadata_rules + result = clone_module.list_metadata_items("metadata_rules", self.mock_target_config) mock_list.assert_called_once() self.assertEqual(result, mock_list.return_value) @patch.object(clone_module, 'create_metadata_item') - @patch.object(clone_module, 'list_metadata_items') - def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_create): + def test_compare_create_metadata_items_existing_fields(self, mock_create): """Test comparing when fields already exist""" mock_source_fields = { 'metadata_fields': [ @@ -210,14 +217,14 @@ def test_compare_create_metadata_items_existing_fields(self, mock_list, mock_cre ] } + mock_source_fields clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") # No fields should be created mock_create.assert_not_called() @patch.object(clone_module, 'create_metadata_item') - @patch.object(clone_module, 'list_metadata_items') - def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_create): + def test_compare_create_metadata_items_existing_rules(self, mock_create): """Test comparing when rules already exist""" mock_source_metadata_rules = { @@ -258,7 +265,7 @@ def test_compare_create_metadata_items_existing_rules(self, mock_list, mock_crea @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing fields""" - mock_source_fields = { + metadata_fields = { 'metadata_fields': [ { 'external_id': 'field1', @@ -283,17 +290,24 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea } ] } + + mock_source_fields= metadata_fields + mock_list.return_value = metadata_fields clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") # Only new_field should be created mock_create.assert_called_once_with('add_metadata_field', mock_source_fields['metadata_fields'][1], self.mock_target_config) + + result = clone_module.list_metadata_items("metadata_fields", self.mock_target_config) + mock_list.assert_called_once() + self.assertEqual(result, mock_list.return_value) @patch.object(clone_module, 'create_metadata_item') @patch.object(clone_module, 'list_metadata_items') def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing rules""" - mock_source_metadata_rules = { + metadata_rules = { 'metadata_rules': [ { 'external_id': 'rule1', @@ -330,11 +344,18 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc } ] } + + mock_source_metadata_rules = metadata_rules + mock_list.return_value = metadata_rules clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") # Only new_rule should be created mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) + result = clone_module.list_metadata_items("metadata_rules", self.mock_target_config) + mock_list.assert_called_once() + self.assertEqual(result, mock_list.return_value) + if __name__ == '__main__': unittest.main() \ No newline at end of file From 3e3d67c7c457c3a7bd97802b36c7031274a43771 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Wed, 16 Jul 2025 12:40:12 +0100 Subject: [PATCH 14/22] fix declarations due to conflict resolution mistake --- cloudinary_cli/modules/clone.py | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/cloudinary_cli/modules/clone.py b/cloudinary_cli/modules/clone.py index 0d0dca1..b9e1dac 100644 --- a/cloudinary_cli/modules/clone.py +++ b/cloudinary_cli/modules/clone.py @@ -62,6 +62,20 @@ def clone(target, force, overwrite, concurrent_workers, fields, if not isinstance(source_assets, dict) or not source_assets.get('resources'): logger.error(style(f"No asset(s) found in {cloudinary.config().cloud_name}", fg="red")) return False + + if 'metadata' in fields: + source_metadata = list_metadata_items("metadata_fields") + if source_metadata.get('metadata_fields'): + target_metadata = list_metadata_items("metadata_fields", config_to_tuple_list(target_config)) + fields_compare = compare_create_metadata_items(source_metadata, target_metadata, config_to_tuple_list(target_config), key="metadata_fields") + source_metadata_rules = list_metadata_items("metadata_rules") + if source_metadata_rules.get('metadata_rules'): + target_metadata_rules = list_metadata_items("metadata_rules", config_to_tuple_list(target_config)) + rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, config_to_tuple_list(target_config), key="metadata_rules", id_field="name") + else: + logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow")) + else: + logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow")) upload_list = _prepare_upload_list( source_assets, target_config, overwrite, async_, @@ -93,19 +107,6 @@ def _validate_clone_inputs(target): "as source environment.") return None, None - if 'metadata' in fields: - source_metadata = list_metadata_items("metadata_fields") - if source_metadata.get('metadata_fields'): - target_metadata = list_metadata_items("metadata_fields", config_to_tuple_list(target_config)) - fields_compare = compare_create_metadata_items(source_metadata, target_metadata, config_to_tuple_list(target_config), key="metadata_fields") - source_metadata_rules = list_metadata_items("metadata_rules") - if source_metadata_rules.get('metadata_rules'): - target_metadata_rules = list_metadata_items("metadata_rules", config_to_tuple_list(target_config)) - rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, config_to_tuple_list(target_config), key="metadata_rules", id_field="name") - else: - logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow")) - else: - logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow")) auth_token = cloudinary.config().auth_token if auth_token: From a2cf5a19ae980418c00c350e6d13ed0acbafbcdd Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Wed, 16 Jul 2025 13:16:02 +0100 Subject: [PATCH 15/22] fix fields declaration for smd --- cloudinary_cli/modules/clone.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cloudinary_cli/modules/clone.py b/cloudinary_cli/modules/clone.py index b9e1dac..73c487b 100644 --- a/cloudinary_cli/modules/clone.py +++ b/cloudinary_cli/modules/clone.py @@ -63,7 +63,7 @@ def clone(target, force, overwrite, concurrent_workers, fields, logger.error(style(f"No asset(s) found in {cloudinary.config().cloud_name}", fg="red")) return False - if 'metadata' in fields: + if 'metadata' in normalize_list_params(fields): source_metadata = list_metadata_items("metadata_fields") if source_metadata.get('metadata_fields'): target_metadata = list_metadata_items("metadata_fields", config_to_tuple_list(target_config)) @@ -160,7 +160,7 @@ def list_metadata_items(method_key, *options): api_name="admin", auto_paginate=True, force=True, return_data=True) - res.get(method_key, []).sort(key=lambda x: x["external_id"]) + res.get(method_key).sort(key=lambda x: x["external_id"]) return res From 2b6fd2817210ed758fe1d9c1636f3f786b1a583d Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Mon, 21 Jul 2025 15:18:44 +0100 Subject: [PATCH 16/22] improve scripts based on discussions --- cloudinary_cli/modules/clone.py | 100 +++------------------- cloudinary_cli/utils/clone/metadata.py | 113 +++++++++++++++++++++++++ cloudinary_cli/utils/config_utils.py | 3 - test/test_modules/test_cli_clone.py | 74 ++++++++-------- 4 files changed, 161 insertions(+), 129 deletions(-) create mode 100644 cloudinary_cli/utils/clone/metadata.py diff --git a/cloudinary_cli/modules/clone.py b/cloudinary_cli/modules/clone.py index 73c487b..9b12b0c 100644 --- a/cloudinary_cli/modules/clone.py +++ b/cloudinary_cli/modules/clone.py @@ -1,11 +1,12 @@ from click import command, option, style, argument from cloudinary_cli.utils.utils import normalize_list_params, print_help_and_exit import cloudinary +from cloudinary_cli.utils.clone.metadata import clone_metadata from cloudinary.auth_token import _digest from cloudinary_cli.utils.utils import run_tasks_concurrently -from cloudinary_cli.utils.api_utils import upload_file, handle_api_command +from cloudinary_cli.utils.api_utils import upload_file from cloudinary_cli.utils.json_utils import print_json -from cloudinary_cli.utils.config_utils import get_cloudinary_config, config_to_dict, config_to_tuple_list +from cloudinary_cli.utils.config_utils import get_cloudinary_config, config_to_dict from cloudinary_cli.defaults import logger from cloudinary_cli.core.search import execute_single_request, handle_auto_pagination import time @@ -55,27 +56,19 @@ def clone(target, force, overwrite, concurrent_workers, fields, target_config, auth_token = _validate_clone_inputs(target) if not target_config: return False - + if 'metadata' in normalize_list_params(fields): + metadata_clone = clone_metadata(target_config) + if not metadata_clone: + logger.error(style(f"The operation has been aborted due to your answer.", fg="red")) + return False + else: + logger.info(style(f"Metadata cloned successfully from {cloudinary.config().cloud_name} to {target_config.cloud_name}. We will now proceed with cloning the assets.", fg="green")) source_assets = search_assets(search_exp, force) if not source_assets: return False if not isinstance(source_assets, dict) or not source_assets.get('resources'): logger.error(style(f"No asset(s) found in {cloudinary.config().cloud_name}", fg="red")) return False - - if 'metadata' in normalize_list_params(fields): - source_metadata = list_metadata_items("metadata_fields") - if source_metadata.get('metadata_fields'): - target_metadata = list_metadata_items("metadata_fields", config_to_tuple_list(target_config)) - fields_compare = compare_create_metadata_items(source_metadata, target_metadata, config_to_tuple_list(target_config), key="metadata_fields") - source_metadata_rules = list_metadata_items("metadata_rules") - if source_metadata_rules.get('metadata_rules'): - target_metadata_rules = list_metadata_items("metadata_rules", config_to_tuple_list(target_config)) - rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, config_to_tuple_list(target_config), key="metadata_rules", id_field="name") - else: - logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow")) - else: - logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow")) upload_list = _prepare_upload_list( source_assets, target_config, overwrite, async_, @@ -150,79 +143,6 @@ def search_assets(search_exp, force): return res -def list_metadata_items(method_key, *options): - api_method_name = 'list_' + method_key - params = [api_method_name] - if options: - options = options[0] - res = handle_api_command(params, (), options, None, None, None, - doc_url="", api_instance=cloudinary.api, - api_name="admin", - auto_paginate=True, - force=True, return_data=True) - res.get(method_key).sort(key=lambda x: x["external_id"]) - - return res - - -def create_metadata_item(api_method_name, item, *options): - params = (api_method_name, item) - if options: - options = options[0] - res = handle_api_command(params, (), options, None, None, None, - doc_url="", api_instance=cloudinary.api, - api_name="admin", - return_data=True) - - return res - - -def deep_diff(obj_source, obj_target): - diffs = {} - for k in set(obj_source.keys()).union(obj_target.keys()): - if obj_source.get(k) != obj_target.get(k): - diffs[k] = {"json_source": obj_source.get(k), "json_target": obj_target.get(k)} - - return diffs - - -def compare_create_metadata_items(json_source, json_target, target_config, key, id_field = "external_id"): - list_source = {item[id_field]: item for item in json_source.get(key, [])} - list_target = {item[id_field]: item for item in json_target.get(key, [])} - - only_in_source = list(list_source.keys() - list_target.keys()) - common = list_source.keys() & list_target.keys() - - if not len(only_in_source): - logger.info(style(f"{(' '.join(key.split('_')))} in {dict(target_config)['cloud_name']} and in {cloudinary.config().cloud_name} are identical. No {(' '.join(key.split('_')))} will be cloned", fg="yellow")) - else: - logger.info(style(f"Copying {len(only_in_source)} {(' '.join(key.split('_')))} from {cloudinary.config().cloud_name} to {dict(target_config)['cloud_name']}", fg="blue")) - - for key_field in only_in_source: - if key == 'metadata_fields': - try: - res = create_metadata_item('add_metadata_field', list_source[key_field],target_config) - logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green")) - except Exception as e: - logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red")) - else: - try: - res = create_metadata_item('add_metadata_rule', list_source[key_field],target_config) - logger.info(style(f"Successfully created {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="green")) - except Exception as e: - logger.error(style(f"Error when creating {(' '.join(key.split('_')))} {key_field} to {dict(target_config)['cloud_name']}", fg="red")) - - - diffs = {} - for id_ in common: - if list_source[id_] != list_target[id_]: - diffs[id_] = deep_diff(list_source[id_], list_target[id_]) - - return { - "only_in_json_source": only_in_source, - "differences": diffs - } - def _normalize_search_expression(search_exp): """ Ensures the search expression has a valid 'type' filter. diff --git a/cloudinary_cli/utils/clone/metadata.py b/cloudinary_cli/utils/clone/metadata.py new file mode 100644 index 0000000..a2ac1f9 --- /dev/null +++ b/cloudinary_cli/utils/clone/metadata.py @@ -0,0 +1,113 @@ +import cloudinary +from cloudinary_cli.utils.config_utils import config_to_dict +from cloudinary_cli.utils.api_utils import handle_auto_pagination +from cloudinary_cli.defaults import logger +from cloudinary_cli.utils.utils import confirm_action +from click import style + +def clone_metadata(config): + """ + Clone metadata from the source to the destination. + """ + target_config = config_to_dict(config) + source_metadata = list_metadata_items("metadata_fields") + if source_metadata.get('metadata_fields'): + target_metadata = list_metadata_items("metadata_fields", **target_config) + fields_compare = compare_create_metadata_items(source_metadata, target_metadata, key="metadata_fields", **target_config) + if not fields_compare: + return False + else: + source_metadata_rules = list_metadata_items("metadata_rules") + if source_metadata_rules.get('metadata_rules'): + target_metadata_rules = list_metadata_items("metadata_rules", **target_config) + rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, key="metadata_rules", id_field="name", **target_config) + if not rules_compare: + return False + else: + logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow")) + else: + logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow")) + + return True # Return True to indicate that the metadata was cloned successfully or False if there were no items to clone. + +def list_metadata_items(method_key, **options): + if method_key == 'metadata_fields': + res = cloudinary.api.list_metadata_fields(**options) + res = handle_auto_pagination(res, cloudinary.api.list_metadata_fields, options, None, force=True, filter_fields="") + else: + res = cloudinary.api.list_metadata_rules(**options) + res = handle_auto_pagination(res, cloudinary.api.list_metadata_rules, options, None, force=True, filter_fields="") + + return res + +def create_metadata_items(api_method_name, item, **options): + if api_method_name == 'add_metadata_field': + res = cloudinary.api.add_metadata_field(item, **options) + else: + res = cloudinary.api.add_metadata_rule(item, **options) + return res + +def deep_diff(obj_source, obj_target): + diffs = {} + for k in set(obj_source.keys()).union(obj_target.keys()): + if obj_source.get(k) != obj_target.get(k): + diffs[k] = {"json_source": obj_source.get(k), "json_target": obj_target.get(k)} + + return diffs + + +def compare_create_metadata_items(json_source, json_target, key, id_field = "external_id", **options): + list_source = {item[id_field]: item for item in json_source.get(key, [])} + list_target = {item[id_field]: item for item in json_target.get(key, [])} + + only_in_source = list(list_source.keys() - list_target.keys()) + common = list_source.keys() & list_target.keys() + + if not len(only_in_source): + logger.info(style(f"{(' '.join(key.split('_')))} in `{dict(options)['cloud_name']}` and in `{cloudinary.config().cloud_name}` are identical. No {(' '.join(key.split('_')))} will be cloned", fg="yellow")) + if not confirm_action( + f"If you had some {key} in the target environment, " + f"new values from the source environment won't be cloned.\n" + f"Would you like to still proceed with the cloning of assets? (y/N).\n"): + logger.info("Stopping.") + return False + else: + logger.info("Continuing.") + else: + logger.info(style(f"{only_in_source} are only in `{dict(options)['cloud_name']}` and will be cloned to `{cloudinary.config().cloud_name}`.", fg="blue")) + if not confirm_action( + f"You have a {key} mismatch between the source and target environment.\n" + f"Confirming this action will create the missing {key} and their values.\n" + f"If you currently have some {key} in the target environment, " + f"new values from the source environment won't be cloned.\n" + f"Continue? (y/N)"): + logger.info("Stopping.") + return False + else: + logger.info("Continuing.") + logger.info(style(f"Copying {len(only_in_source)} {(' '.join(key.split('_')))} from {cloudinary.config().cloud_name} to {dict(options)['cloud_name']}", fg="blue")) + for key_field in only_in_source: + if key == 'metadata_fields': + try: + res = create_metadata_items('add_metadata_field', list_source[key_field], **options) + logger.info(style(f"Successfully created {(' '.join(key.split('_')))[:-1]} `{res.get('label')}` to {dict(options)['cloud_name']}", fg="green")) + except Exception as e: + logger.error(style(f"Error when creating {(' '.join(key.split('_')))[:-1]} `{res.get('label')}`` to {dict(options)['cloud_name']}", fg="red")) + else: + try: + res = create_metadata_items('add_metadata_rule', list_source[key_field],**options) + logger.info(style(f"Successfully created {(' '.join(key.split('_')))[:-1]} `{res.get('name')}` to {dict(options)['cloud_name']}", fg="green")) + except Exception as e: + logger.error(style(f"Error when creating {(' '.join(key.split('_')))[:-1]} `{res.get('name')}` to {dict(options)['cloud_name']}", fg="red")) + + # for Phase 3 + #diffs = {} + #for id_ in common: + # if list_source[id_] != list_target[id_]: + # diffs[id_] = deep_diff(list_source[id_], list_target[id_]) + + #return { + # "only_in_json_source": only_in_source, + # "differences": diffs + #} + return True # Return True to indicate that the metadata items were compared and created successfully. \ No newline at end of file diff --git a/cloudinary_cli/utils/config_utils.py b/cloudinary_cli/utils/config_utils.py index 28d1564..7b5732a 100644 --- a/cloudinary_cli/utils/config_utils.py +++ b/cloudinary_cli/utils/config_utils.py @@ -67,9 +67,6 @@ def get_cloudinary_config(target): def config_to_dict(config): return {k: v for k, v in config.__dict__.items() if not k.startswith("_")} -def config_to_tuple_list(config): - return [(k, v) for k, v in config.__dict__.items() if not k.startswith("_")] - def show_cloudinary_config(cloudinary_config): obfuscated_config = config_to_dict(cloudinary_config) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index c490a23..06a5b72 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -8,6 +8,8 @@ import cloudinary_cli.modules clone_module = sys.modules['cloudinary_cli.modules.clone'] +import cloudinary_cli.utils.clone.metadata as clone_metadata_utils + from cloudinary_cli.defaults import logger @@ -35,7 +37,7 @@ def setUp(self): 'api_secret': 'target-secret' } - @patch.object(clone_module, 'list_metadata_items') + @patch.object(clone_metadata_utils, 'list_metadata_items') def test_list_metadata_items(self, mock_metadata_fields): """Test listing metadata fields""" mock_metadata_fields.return_value = { @@ -49,12 +51,12 @@ def test_list_metadata_items(self, mock_metadata_fields): ] } - result = clone_module.list_metadata_items("metadata_fields") + result = clone_metadata_utils.list_metadata_items("metadata_fields") mock_metadata_fields.assert_called_once() self.assertEqual(result, mock_metadata_fields.return_value) - @patch.object(clone_module, 'list_metadata_items') + @patch.object(clone_metadata_utils, 'list_metadata_items') def test_list_metadata_rules(self, mock_metadata_rules): """Test listing metadata fields""" mock_metadata_rules.return_value = { @@ -73,12 +75,12 @@ def test_list_metadata_rules(self, mock_metadata_rules): ] } - result = clone_module.list_metadata_items("metadata_rules") + result = clone_metadata_utils.list_metadata_items("metadata_rules") mock_metadata_rules.assert_called_once() self.assertEqual(result, mock_metadata_rules.return_value) - @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_metadata_utils, 'create_metadata_items') def test_create_metadata_item_field(self, mock_add_metadata_field): """Test creating a single metadata field""" mock_metadata_field = { @@ -88,11 +90,11 @@ def test_create_metadata_item_field(self, mock_add_metadata_field): 'mandatory': False } - clone_module.create_metadata_item('add_metadata_field', mock_metadata_field) + clone_metadata_utils.create_metadata_items('add_metadata_field', mock_metadata_field) mock_add_metadata_field.assert_called_once_with('add_metadata_field', mock_metadata_field) - @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_metadata_utils, 'create_metadata_items') def test_create_metadata_item_rule(self, mock_add_metadata_rule): """Test creating a single metadata rule""" mock_metadata_rule = { @@ -107,12 +109,12 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): }] } - clone_module.create_metadata_item('add_metadata_rule', mock_metadata_rule) + clone_metadata_utils.create_metadata_items('add_metadata_rule', mock_metadata_rule) mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rule) - @patch.object(clone_module, 'create_metadata_item') - @patch.object(clone_module, 'list_metadata_items') + @patch.object(clone_metadata_utils, 'create_metadata_items') + @patch.object(clone_metadata_utils, 'list_metadata_items') def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): """Test comparing and creating new metadata fields""" metadata_fields = { @@ -136,19 +138,19 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): 'metadata_fields': [] } - clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") - + res = clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) + print(res) # Both fields should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][0], self.mock_target_config) - mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][1], self.mock_target_config) + mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][0], **self.mock_target_config) + mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][1], **self.mock_target_config) - result = clone_module.list_metadata_items("metadata_fields", self.mock_target_config) + result = clone_metadata_utils.list_metadata_items("metadata_fields", **self.mock_target_config) mock_list.assert_called_once() self.assertEqual(result, mock_list.return_value) - @patch.object(clone_module, 'create_metadata_item') - @patch.object(clone_module, 'list_metadata_items') + @patch.object(clone_metadata_utils, 'create_metadata_items') + @patch.object(clone_metadata_utils, 'list_metadata_items') def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): """Test comparing and creating new metadata rules""" metadata_rules = { @@ -181,18 +183,18 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): 'metadata_rules': [] } - clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") - + res = clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) + print(res) # Both rules should be created self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], self.mock_target_config) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], **self.mock_target_config) + mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], **self.mock_target_config) - result = clone_module.list_metadata_items("metadata_rules", self.mock_target_config) + result = clone_metadata_utils.list_metadata_items("metadata_rules", **self.mock_target_config) mock_list.assert_called_once() self.assertEqual(result, mock_list.return_value) - @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_metadata_utils, 'create_metadata_items') def test_compare_create_metadata_items_existing_fields(self, mock_create): """Test comparing when fields already exist""" mock_source_fields = { @@ -217,12 +219,12 @@ def test_compare_create_metadata_items_existing_fields(self, mock_create): } mock_source_fields - clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") + clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) # No fields should be created mock_create.assert_not_called() - @patch.object(clone_module, 'create_metadata_item') + @patch.object(clone_metadata_utils, 'create_metadata_items') def test_compare_create_metadata_items_existing_rules(self, mock_create): """Test comparing when rules already exist""" @@ -255,13 +257,13 @@ def test_compare_create_metadata_items_existing_rules(self, mock_create): ] } - clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") + clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # No rules should be created mock_create.assert_not_called() - @patch.object(clone_module, 'create_metadata_item') - @patch.object(clone_module, 'list_metadata_items') + @patch.object(clone_metadata_utils, 'create_metadata_items') + @patch.object(clone_metadata_utils, 'list_metadata_items') def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing fields""" metadata_fields = { @@ -293,17 +295,17 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea mock_source_fields= metadata_fields mock_list.return_value = metadata_fields - clone_module.compare_create_metadata_items(mock_source_fields, mock_destination_fields, self.mock_target_config, key="metadata_fields") + clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) # Only new_field should be created - mock_create.assert_called_once_with('add_metadata_field', mock_source_fields['metadata_fields'][1], self.mock_target_config) + mock_create.assert_called_once_with('add_metadata_field', mock_source_fields['metadata_fields'][1], **self.mock_target_config) - result = clone_module.list_metadata_items("metadata_fields", self.mock_target_config) + result = clone_metadata_utils.list_metadata_items("metadata_fields", **self.mock_target_config) mock_list.assert_called_once() self.assertEqual(result, mock_list.return_value) - @patch.object(clone_module, 'create_metadata_item') - @patch.object(clone_module, 'list_metadata_items') + @patch.object(clone_metadata_utils, 'create_metadata_items') + @patch.object(clone_metadata_utils, 'list_metadata_items') def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing rules""" metadata_rules = { @@ -347,12 +349,12 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc mock_source_metadata_rules = metadata_rules mock_list.return_value = metadata_rules - clone_module.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, self.mock_target_config, key="metadata_rules") + clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # Only new_rule should be created - mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], self.mock_target_config) + mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], **self.mock_target_config) - result = clone_module.list_metadata_items("metadata_rules", self.mock_target_config) + result = clone_metadata_utils.list_metadata_items("metadata_rules", **self.mock_target_config) mock_list.assert_called_once() self.assertEqual(result, mock_list.return_value) From 23febe2d02ae23249f5df5e32bb923ccb33a0506 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Mon, 21 Jul 2025 15:35:05 +0100 Subject: [PATCH 17/22] tentatively add user input mock --- test/test_modules/test_cli_clone.py | 31 +++++++++++++++++------------ 1 file changed, 18 insertions(+), 13 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index 06a5b72..edf8795 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -115,7 +115,8 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): + @patch('cloudinary_cli.utils.utils.confirm_action') + def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create, mock_confirm): """Test comparing and creating new metadata fields""" metadata_fields = { 'metadata_fields': [ @@ -131,7 +132,7 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): } ] } - + mock_confirm.return_value = True mock_source_fields = metadata_fields mock_list.return_value = metadata_fields mock_destination_fields = { @@ -151,7 +152,8 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): + @patch('cloudinary_cli.utils.utils.confirm_action') + def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create, mock_confirm): """Test comparing and creating new metadata rules""" metadata_rules = { 'metadata_rules': [ @@ -175,7 +177,7 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): } ] } - + mock_confirm.return_value = True mock_source_metadata_rules = metadata_rules mock_list.return_value = metadata_rules @@ -195,7 +197,8 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): self.assertEqual(result, mock_list.return_value) @patch.object(clone_metadata_utils, 'create_metadata_items') - def test_compare_create_metadata_items_existing_fields(self, mock_create): + @patch('cloudinary_cli.utils.utils.confirm_action') + def test_compare_create_metadata_items_existing_fields(self, mock_create, mock_confirm): """Test comparing when fields already exist""" mock_source_fields = { 'metadata_fields': [ @@ -217,15 +220,15 @@ def test_compare_create_metadata_items_existing_fields(self, mock_create): } ] } - - mock_source_fields + mock_confirm.return_value = True clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) # No fields should be created mock_create.assert_not_called() @patch.object(clone_metadata_utils, 'create_metadata_items') - def test_compare_create_metadata_items_existing_rules(self, mock_create): + @patch('cloudinary_cli.utils.utils.confirm_action') + def test_compare_create_metadata_items_existing_rules(self, mock_create, mock_confirm): """Test comparing when rules already exist""" mock_source_metadata_rules = { @@ -256,7 +259,7 @@ def test_compare_create_metadata_items_existing_rules(self, mock_create): } ] } - + mock_confirm.return_value = True clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # No rules should be created @@ -264,7 +267,8 @@ def test_compare_create_metadata_items_existing_rules(self, mock_create): @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): + @patch('cloudinary_cli.utils.utils.confirm_action') + def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create, mock_confirm): """Test comparing with mix of new and existing fields""" metadata_fields = { 'metadata_fields': [ @@ -291,7 +295,7 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea } ] } - + mock_confirm.return_value = True mock_source_fields= metadata_fields mock_list.return_value = metadata_fields @@ -306,7 +310,8 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): + @patch('cloudinary_cli.utils.utils.confirm_action') + def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create, mock_confirm): """Test comparing with mix of new and existing rules""" metadata_rules = { 'metadata_rules': [ @@ -345,7 +350,7 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc } ] } - + mock_confirm.return_value = True mock_source_metadata_rules = metadata_rules mock_list.return_value = metadata_rules From b074d0e1550da0ed68ec6261091b3e0485ce0d57 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Mon, 21 Jul 2025 15:42:31 +0100 Subject: [PATCH 18/22] tentatively add user input mock - 2 --- test/test_modules/test_cli_clone.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index edf8795..b5d0771 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -116,7 +116,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') @patch('cloudinary_cli.utils.utils.confirm_action') - def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create, mock_confirm): + def test_compare_create_metadata_items_new_fields(self,mock_confirm, mock_list, mock_create, ): """Test comparing and creating new metadata fields""" metadata_fields = { 'metadata_fields': [ @@ -139,8 +139,8 @@ def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create, 'metadata_fields': [] } - res = clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) - print(res) + clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) + mock_confirm.return_value = True # Both fields should be created self.assertEqual(mock_create.call_count, 2) mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][0], **self.mock_target_config) @@ -185,8 +185,7 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create, m 'metadata_rules': [] } - res = clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) - print(res) + clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # Both rules should be created self.assertEqual(mock_create.call_count, 2) mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], **self.mock_target_config) From 17981a8601cbe28a5cfc6213aee0fccbf1780033 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Mon, 21 Jul 2025 15:48:03 +0100 Subject: [PATCH 19/22] tentatively add user input mock - 3 --- test/test_modules/test_cli_clone.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index b5d0771..fbef707 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -115,7 +115,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('cloudinary_cli.utils.utils.confirm_action') + @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_new_fields(self,mock_confirm, mock_list, mock_create, ): """Test comparing and creating new metadata fields""" metadata_fields = { @@ -132,7 +132,7 @@ def test_compare_create_metadata_items_new_fields(self,mock_confirm, mock_list, } ] } - mock_confirm.return_value = True + mock_source_fields = metadata_fields mock_list.return_value = metadata_fields mock_destination_fields = { @@ -140,7 +140,7 @@ def test_compare_create_metadata_items_new_fields(self,mock_confirm, mock_list, } clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) - mock_confirm.return_value = True + # Both fields should be created self.assertEqual(mock_create.call_count, 2) mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][0], **self.mock_target_config) @@ -152,7 +152,7 @@ def test_compare_create_metadata_items_new_fields(self,mock_confirm, mock_list, @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('cloudinary_cli.utils.utils.confirm_action') + @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create, mock_confirm): """Test comparing and creating new metadata rules""" metadata_rules = { @@ -177,7 +177,7 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create, m } ] } - mock_confirm.return_value = True + mock_source_metadata_rules = metadata_rules mock_list.return_value = metadata_rules @@ -196,7 +196,7 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create, m self.assertEqual(result, mock_list.return_value) @patch.object(clone_metadata_utils, 'create_metadata_items') - @patch('cloudinary_cli.utils.utils.confirm_action') + @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_existing_fields(self, mock_create, mock_confirm): """Test comparing when fields already exist""" mock_source_fields = { @@ -219,14 +219,14 @@ def test_compare_create_metadata_items_existing_fields(self, mock_create, mock_c } ] } - mock_confirm.return_value = True + clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) # No fields should be created mock_create.assert_not_called() @patch.object(clone_metadata_utils, 'create_metadata_items') - @patch('cloudinary_cli.utils.utils.confirm_action') + @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_existing_rules(self, mock_create, mock_confirm): """Test comparing when rules already exist""" @@ -258,7 +258,7 @@ def test_compare_create_metadata_items_existing_rules(self, mock_create, mock_co } ] } - mock_confirm.return_value = True + clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # No rules should be created @@ -266,7 +266,7 @@ def test_compare_create_metadata_items_existing_rules(self, mock_create, mock_co @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('cloudinary_cli.utils.utils.confirm_action') + @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create, mock_confirm): """Test comparing with mix of new and existing fields""" metadata_fields = { @@ -294,7 +294,7 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea } ] } - mock_confirm.return_value = True + mock_source_fields= metadata_fields mock_list.return_value = metadata_fields @@ -309,7 +309,7 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('cloudinary_cli.utils.utils.confirm_action') + @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create, mock_confirm): """Test comparing with mix of new and existing rules""" metadata_rules = { @@ -349,7 +349,7 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc } ] } - mock_confirm.return_value = True + mock_source_metadata_rules = metadata_rules mock_list.return_value = metadata_rules From fe0233ea9c177e927bf2c3ce9f15ff86dce46a0b Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Mon, 21 Jul 2025 15:53:33 +0100 Subject: [PATCH 20/22] tentatively add user input mock - 4 --- test/test_modules/test_cli_clone.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index fbef707..5b94153 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -116,7 +116,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') @patch('builtins.input', return_value='true') - def test_compare_create_metadata_items_new_fields(self,mock_confirm, mock_list, mock_create, ): + def test_compare_create_metadata_items_new_fields(sel, mock_list, mock_create, ): """Test comparing and creating new metadata fields""" metadata_fields = { 'metadata_fields': [ @@ -152,8 +152,8 @@ def test_compare_create_metadata_items_new_fields(self,mock_confirm, mock_list, @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('builtins.input', return_value='true') - def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create, mock_confirm): + @patch('builtins.input', return_value='y') + def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): """Test comparing and creating new metadata rules""" metadata_rules = { 'metadata_rules': [ @@ -197,7 +197,7 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create, m @patch.object(clone_metadata_utils, 'create_metadata_items') @patch('builtins.input', return_value='true') - def test_compare_create_metadata_items_existing_fields(self, mock_create, mock_confirm): + def test_compare_create_metadata_items_existing_fields(self, mock_create): """Test comparing when fields already exist""" mock_source_fields = { 'metadata_fields': [ @@ -227,7 +227,7 @@ def test_compare_create_metadata_items_existing_fields(self, mock_create, mock_c @patch.object(clone_metadata_utils, 'create_metadata_items') @patch('builtins.input', return_value='true') - def test_compare_create_metadata_items_existing_rules(self, mock_create, mock_confirm): + def test_compare_create_metadata_items_existing_rules(self, mock_create): """Test comparing when rules already exist""" mock_source_metadata_rules = { @@ -267,7 +267,7 @@ def test_compare_create_metadata_items_existing_rules(self, mock_create, mock_co @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') @patch('builtins.input', return_value='true') - def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create, mock_confirm): + def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing fields""" metadata_fields = { 'metadata_fields': [ @@ -310,7 +310,7 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') @patch('builtins.input', return_value='true') - def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create, mock_confirm): + def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing rules""" metadata_rules = { 'metadata_rules': [ From a626d189491443e6114aadf13159021bdb1bbdf9 Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Mon, 21 Jul 2025 16:00:48 +0100 Subject: [PATCH 21/22] tentatively add user input mock - 5 --- test/test_modules/test_cli_clone.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index 5b94153..cd25f07 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -115,8 +115,7 @@ def test_create_metadata_item_rule(self, mock_add_metadata_rule): @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('builtins.input', return_value='true') - def test_compare_create_metadata_items_new_fields(sel, mock_list, mock_create, ): + def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): """Test comparing and creating new metadata fields""" metadata_fields = { 'metadata_fields': [ @@ -139,7 +138,8 @@ def test_compare_create_metadata_items_new_fields(sel, mock_list, mock_create, ) 'metadata_fields': [] } - clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) + with patch('builtins.input', return_value='y'): + clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) # Both fields should be created self.assertEqual(mock_create.call_count, 2) @@ -152,7 +152,6 @@ def test_compare_create_metadata_items_new_fields(sel, mock_list, mock_create, ) @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('builtins.input', return_value='y') def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): """Test comparing and creating new metadata rules""" metadata_rules = { @@ -185,7 +184,8 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): 'metadata_rules': [] } - clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) + with patch('builtins.input', return_value='y'): + clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # Both rules should be created self.assertEqual(mock_create.call_count, 2) mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], **self.mock_target_config) @@ -196,7 +196,6 @@ def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): self.assertEqual(result, mock_list.return_value) @patch.object(clone_metadata_utils, 'create_metadata_items') - @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_existing_fields(self, mock_create): """Test comparing when fields already exist""" mock_source_fields = { @@ -220,13 +219,13 @@ def test_compare_create_metadata_items_existing_fields(self, mock_create): ] } - clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) + with patch('builtins.input', return_value='y'): + clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) # No fields should be created mock_create.assert_not_called() @patch.object(clone_metadata_utils, 'create_metadata_items') - @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_existing_rules(self, mock_create): """Test comparing when rules already exist""" @@ -259,14 +258,14 @@ def test_compare_create_metadata_items_existing_rules(self, mock_create): ] } - clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) + with patch('builtins.input', return_value='y'): + clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # No rules should be created mock_create.assert_not_called() @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing fields""" metadata_fields = { @@ -298,7 +297,8 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea mock_source_fields= metadata_fields mock_list.return_value = metadata_fields - clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) + with patch('builtins.input', return_value='y'): + clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) # Only new_field should be created mock_create.assert_called_once_with('add_metadata_field', mock_source_fields['metadata_fields'][1], **self.mock_target_config) @@ -309,7 +309,6 @@ def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_crea @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - @patch('builtins.input', return_value='true') def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): """Test comparing with mix of new and existing rules""" metadata_rules = { @@ -353,7 +352,8 @@ def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, moc mock_source_metadata_rules = metadata_rules mock_list.return_value = metadata_rules - clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) + with patch('builtins.input', return_value='y'): + clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) # Only new_rule should be created mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], **self.mock_target_config) From 30c8f6b1056fd3c339eae6bb62fb31171739b87f Mon Sep 17 00:00:00 2001 From: Vdeub-cloudinary Date: Thu, 18 Dec 2025 16:03:26 +0000 Subject: [PATCH 22/22] revamp code to make it more reusable and improve tests --- cloudinary_cli/modules/clone.py | 7 +- cloudinary_cli/utils/api_utils.py | 2 + cloudinary_cli/utils/clone/metadata.py | 223 ++++++++------ cloudinary_cli/utils/utils.py | 24 ++ test/test_modules/test_cli_clone.py | 397 +++++++------------------ 5 files changed, 258 insertions(+), 395 deletions(-) diff --git a/cloudinary_cli/modules/clone.py b/cloudinary_cli/modules/clone.py index 9b12b0c..f7d97f0 100644 --- a/cloudinary_cli/modules/clone.py +++ b/cloudinary_cli/modules/clone.py @@ -25,7 +25,7 @@ Format: cld clone `` can be a CLOUDINARY_URL or a saved config (see `config` command) Example 1 (Copy all assets including tags and context using CLOUDINARY URL): - cld clone cloudinary://:@ -fi tags,context + cld clone cloudinary://:@ -fi tags,context,metadata Example 2 (Copy all assets with a specific tag via a search expression using a saved config): cld clone -se "tags:" """) @@ -57,12 +57,11 @@ def clone(target, force, overwrite, concurrent_workers, fields, if not target_config: return False if 'metadata' in normalize_list_params(fields): - metadata_clone = clone_metadata(target_config) + metadata_clone = clone_metadata(target_config, force) if not metadata_clone: - logger.error(style(f"The operation has been aborted due to your answer.", fg="red")) return False else: - logger.info(style(f"Metadata cloned successfully from {cloudinary.config().cloud_name} to {target_config.cloud_name}. We will now proceed with cloning the assets.", fg="green")) + logger.info(style(f"The metadata process from {cloudinary.config().cloud_name} to {target_config.cloud_name} is now done. We will now proceed with cloning the assets.", fg="green")) source_assets = search_assets(search_exp, force) if not source_assets: return False diff --git a/cloudinary_cli/utils/api_utils.py b/cloudinary_cli/utils/api_utils.py index 8927b43..0c79889 100644 --- a/cloudinary_cli/utils/api_utils.py +++ b/cloudinary_cli/utils/api_utils.py @@ -366,3 +366,5 @@ def handle_auto_pagination(res, func, args, kwargs, force, filter_fields): all_results.pop(cursor_field, None) return all_results + + diff --git a/cloudinary_cli/utils/clone/metadata.py b/cloudinary_cli/utils/clone/metadata.py index a2ac1f9..ac26722 100644 --- a/cloudinary_cli/utils/clone/metadata.py +++ b/cloudinary_cli/utils/clone/metadata.py @@ -1,113 +1,144 @@ import cloudinary from cloudinary_cli.utils.config_utils import config_to_dict -from cloudinary_cli.utils.api_utils import handle_auto_pagination +from cloudinary_cli.utils.api_utils import call_api from cloudinary_cli.defaults import logger -from cloudinary_cli.utils.utils import confirm_action +from cloudinary_cli.utils.utils import confirm_action, compare_dicts from click import style -def clone_metadata(config): - """ - Clone metadata from the source to the destination. - """ - target_config = config_to_dict(config) - source_metadata = list_metadata_items("metadata_fields") - if source_metadata.get('metadata_fields'): - target_metadata = list_metadata_items("metadata_fields", **target_config) - fields_compare = compare_create_metadata_items(source_metadata, target_metadata, key="metadata_fields", **target_config) - if not fields_compare: - return False - else: - source_metadata_rules = list_metadata_items("metadata_rules") - if source_metadata_rules.get('metadata_rules'): - target_metadata_rules = list_metadata_items("metadata_rules", **target_config) - rules_compare = compare_create_metadata_items(source_metadata_rules,target_metadata_rules, key="metadata_rules", id_field="name", **target_config) - if not rules_compare: - return False - else: - logger.info(style(f"No metadata rules found in {cloudinary.config().cloud_name}", fg="yellow")) - else: - logger.info(style(f"No metadata found in {cloudinary.config().cloud_name}", fg="yellow")) - - return True # Return True to indicate that the metadata was cloned successfully or False if there were no items to clone. - -def list_metadata_items(method_key, **options): - if method_key == 'metadata_fields': - res = cloudinary.api.list_metadata_fields(**options) - res = handle_auto_pagination(res, cloudinary.api.list_metadata_fields, options, None, force=True, filter_fields="") - else: - res = cloudinary.api.list_metadata_rules(**options) - res = handle_auto_pagination(res, cloudinary.api.list_metadata_rules, options, None, force=True, filter_fields="") - - return res - -def create_metadata_items(api_method_name, item, **options): - if api_method_name == 'add_metadata_field': - res = cloudinary.api.add_metadata_field(item, **options) - else: - res = cloudinary.api.add_metadata_rule(item, **options) - return res +METADATA_FIELDS = "fields" +METADATA_RULES = "rules" +COMPARE_KEY_FIELDS = "external_id" +COMPARE_KEY_RULES = "name" +METADATA_TYPE_SINGULAR = { + "fields": "field", + "rules": "rule" +} +METADATA_API_METHODS = { + "fields": cloudinary.api.add_metadata_field, + "rules": cloudinary.api.add_metadata_rule +} -def deep_diff(obj_source, obj_target): - diffs = {} - for k in set(obj_source.keys()).union(obj_target.keys()): - if obj_source.get(k) != obj_target.get(k): - diffs[k] = {"json_source": obj_source.get(k), "json_target": obj_target.get(k)} +def clone_metadata(config, force): + """Clone metadata fields and rules from source to target.""" + target_config = config_to_dict(config) + + # Clone fields (required) + fields_result = _clone_metadata_type(METADATA_FIELDS, COMPARE_KEY_FIELDS, target_config, force) + if fields_result is False: + return False - return diffs + # Clone rules (optional) + rules_result = _clone_metadata_type(METADATA_RULES, COMPARE_KEY_RULES, target_config, force) + if rules_result is False: + return False + + return True +def _clone_metadata_type(item_type, compare_key, target_config, force): + """ + Generic function to clone a metadata type (fields or rules). + + :param item_type: 'fields' or 'rules' + :param compare_key: Key to use for comparison ('external_id' or 'name') + :param target_config: Target configuration dict + :param force: Skip confirmation if True + :return: True on success, False on failure, None if nothing to clone + """ + source_cloud = cloudinary.config().cloud_name + target_cloud = target_config['cloud_name'] + + # List source items + logger.info(style(f"Listing metadata {item_type} in `{source_cloud}`.", fg="blue")) + source_items = list_metadata_items(item_type) + + if not source_items: + logger.info(style(f"No metadata {item_type} found in `{source_cloud}`.", fg="yellow")) + return False + + logger.info(style(f"{len(source_items)} metadata {item_type} found in `{source_cloud}`.", fg="green")) + + # List target items + logger.info(style(f"Listing metadata {item_type} in `{target_cloud}`.", fg="blue")) + target_items = list_metadata_items(item_type, **target_config) + logger.info(style(f"{len(target_items)} metadata {item_type} found in `{target_cloud}`.", fg="green")) + + # Compare and sync + source_map, only_in_source, common = compare_dicts(source_items, target_items, compare_key) + return sync_metadata_items(source_map, only_in_source, common, item_type, force, **target_config) -def compare_create_metadata_items(json_source, json_target, key, id_field = "external_id", **options): - list_source = {item[id_field]: item for item in json_source.get(key, [])} - list_target = {item[id_field]: item for item in json_target.get(key, [])} +def list_metadata_items(item_type, **options): + """ + List metadata fields or rules. + + :param item_type: Either 'fields' or 'rules' + :param options: Cloudinary API options (cloud_name, api_key, etc.) + :return: List of metadata items + """ + api_method = getattr(cloudinary.api, f'list_metadata_{item_type}') + res = api_method(**options) + return res.get(f'metadata_{item_type}', []) - only_in_source = list(list_source.keys() - list_target.keys()) - common = list_source.keys() & list_target.keys() +def sync_metadata_items(source_metadata_items, only_in_source_items, common_items, item_type, force, **options): + source_cloud = cloudinary.config().cloud_name + target_cloud = options['cloud_name'] + succeeded = [] + failed = [] - if not len(only_in_source): - logger.info(style(f"{(' '.join(key.split('_')))} in `{dict(options)['cloud_name']}` and in `{cloudinary.config().cloud_name}` are identical. No {(' '.join(key.split('_')))} will be cloned", fg="yellow")) - if not confirm_action( - f"If you had some {key} in the target environment, " - f"new values from the source environment won't be cloned.\n" - f"Would you like to still proceed with the cloning of assets? (y/N).\n"): - logger.info("Stopping.") - return False - else: - logger.info("Continuing.") - else: - logger.info(style(f"{only_in_source} are only in `{dict(options)['cloud_name']}` and will be cloned to `{cloudinary.config().cloud_name}`.", fg="blue")) + + if not only_in_source_items: + logger.info(style( + f"All metadata {item_type} from `{source_cloud}` already exist in `{target_cloud}`. " + f"No metadata {item_type} cloning needed.", + fg="yellow" + )) + return True + + logger.info(style( + f"Metadata {item_type} {only_in_source_items} will be cloned from `{source_cloud}` to `{target_cloud}`.", + fg="yellow" + )) + + if common_items: + logger.info(style( + f"Metadata {item_type} {list(common_items)} exist in both clouds and will be skipped.", + fg="yellow" + )) + if not force: if not confirm_action( - f"You have a {key} mismatch between the source and target environment.\n" - f"Confirming this action will create the missing {key} and their values.\n" - f"If you currently have some {key} in the target environment, " - f"new values from the source environment won't be cloned.\n" + f"Based on the analysis above, \n" + f"The module will now copy the metadata {item_type} from {cloudinary.config().cloud_name} to {dict(options)['cloud_name']}.\n" f"Continue? (y/N)"): logger.info("Stopping.") return False else: - logger.info("Continuing.") - logger.info(style(f"Copying {len(only_in_source)} {(' '.join(key.split('_')))} from {cloudinary.config().cloud_name} to {dict(options)['cloud_name']}", fg="blue")) - for key_field in only_in_source: - if key == 'metadata_fields': - try: - res = create_metadata_items('add_metadata_field', list_source[key_field], **options) - logger.info(style(f"Successfully created {(' '.join(key.split('_')))[:-1]} `{res.get('label')}` to {dict(options)['cloud_name']}", fg="green")) - except Exception as e: - logger.error(style(f"Error when creating {(' '.join(key.split('_')))[:-1]} `{res.get('label')}`` to {dict(options)['cloud_name']}", fg="red")) - else: - try: - res = create_metadata_items('add_metadata_rule', list_source[key_field],**options) - logger.info(style(f"Successfully created {(' '.join(key.split('_')))[:-1]} `{res.get('name')}` to {dict(options)['cloud_name']}", fg="green")) - except Exception as e: - logger.error(style(f"Error when creating {(' '.join(key.split('_')))[:-1]} `{res.get('name')}` to {dict(options)['cloud_name']}", fg="red")) - - # for Phase 3 - #diffs = {} - #for id_ in common: - # if list_source[id_] != list_target[id_]: - # diffs[id_] = deep_diff(list_source[id_], list_target[id_]) + logger.info("Continuing. You may use the -F " + "flag to skip confirmation.") - #return { - # "only_in_json_source": only_in_source, - # "differences": diffs - #} - return True # Return True to indicate that the metadata items were compared and created successfully. \ No newline at end of file + add_method = METADATA_API_METHODS.get(item_type) + singular = METADATA_TYPE_SINGULAR.get(item_type) + + for key_field in only_in_source_items: + try: + add_method(source_metadata_items[key_field], **options) + succeeded.append(key_field) + logger.info(style(f"Successfully created metadata {singular} `{key_field}` in `{target_cloud}`", fg="green")) + except Exception as e: + failed.append((key_field, str(e))) + logger.error(style( + f"Failed to create metadata {singular} `{key_field}` in `{target_cloud}`: {e}", + fg="red" + )) + + # Summary + if failed: + logger.warning(style( + f"Cloned {len(succeeded)}/{len(only_in_source_items)} {item_type} successfully. " + f"{len(failed)} failed.", + fg="yellow" + )) + return False # Or consider partial success handling + + if succeeded: + logger.info(style(f"Successfully cloned {len(succeeded)} metadata {item_type}.", fg="green")) + + return True \ No newline at end of file diff --git a/cloudinary_cli/utils/utils.py b/cloudinary_cli/utils/utils.py index e0c69a4..3e77955 100644 --- a/cloudinary_cli/utils/utils.py +++ b/cloudinary_cli/utils/utils.py @@ -401,3 +401,27 @@ def split_opt(opt): if opt[1:2] == first: return opt[:2], opt[2:] return first, opt[1:] + + +def compare_dicts(dict1, dict2, compare_key):# + """ + Diff between two dictionaries. + + This function is used to compare two dictionaries and return the keys that are only in the first dictionary, + the keys that are only in the second dictionary, and the keys that are in both dictionaries. + The compare_key is a unique key to compare the dictionaries by. + For Phase 3 - add deep diff between two lists of dictionaries. + Example for phase 3: compare metadata fields and their datasource + diffs = {} + for k in set(dict1.keys()).union(dict2.keys()): + if dict1.get(k) != dict2.get(k): + diffs[k] = {"json_source": dict1.get(k), "json_target": dict2.get(k)} + """ + list_dict1 = {item[compare_key]: item for item in dict1} + list_dict2 = {item[compare_key]: item for item in dict2} + + only_in_dict1 = list(list_dict1.keys() - list_dict2.keys()) + #only_in_dict2 = list(list_dict2.keys() - list_dict1.keys()) not needed for now + common = list_dict1.keys() & list_dict2.keys() + + return list_dict1, only_in_dict1, common \ No newline at end of file diff --git a/test/test_modules/test_cli_clone.py b/test/test_modules/test_cli_clone.py index cd25f07..9af3bc7 100644 --- a/test/test_modules/test_cli_clone.py +++ b/test/test_modules/test_cli_clone.py @@ -11,6 +11,7 @@ import cloudinary_cli.utils.clone.metadata as clone_metadata_utils from cloudinary_cli.defaults import logger +from types import SimpleNamespace class TestCLIClone(unittest.TestCase): @@ -36,22 +37,41 @@ def setUp(self): 'api_key': 'target-key', 'api_secret': 'target-secret' } + self.mock_source_config = { + 'cloud_name': 'source-cloud', + 'api_key': 'source-key', + 'api_secret': 'source-secret' + } + self.mock_fields_result = [ + { + 'external_id': 'test_field', + 'type': 'string', + 'label': 'Test Field', + 'mandatory': False + } + ] + self.mock_rules_result = [ + { + 'name': 'test_rule', + 'condition': 'if', + 'metadata_field': { + 'external_id': 'test_field' + }, + 'results': [{ + 'value': 'test_value', + 'apply_to': ['metadata_field_external_id'] + }] + } + ] @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_list_metadata_items(self, mock_metadata_fields): + def test_list_metadata_fields(self, mock_metadata_fields): """Test listing metadata fields""" mock_metadata_fields.return_value = { - 'metadata_fields': [ - { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False - } - ] + 'metadata_fields': self.mock_fields_result } - result = clone_metadata_utils.list_metadata_items("metadata_fields") + result = clone_metadata_utils.list_metadata_items('fields') mock_metadata_fields.assert_called_once() self.assertEqual(result, mock_metadata_fields.return_value) @@ -60,307 +80,95 @@ def test_list_metadata_items(self, mock_metadata_fields): def test_list_metadata_rules(self, mock_metadata_rules): """Test listing metadata fields""" mock_metadata_rules.return_value = { - 'metadata_rules': [ - { - 'external_id': 'test_rule', - 'condition': 'if', - 'metadata_field': { - 'external_id': 'test_field' - }, - 'results': [{ - 'value': 'test_value', - 'apply_to': ['metadata_field_external_id'] - }] - } - ] + 'metadata_rules': self.mock_rules_result } - result = clone_metadata_utils.list_metadata_items("metadata_rules") + result = clone_metadata_utils.list_metadata_items('rules') - mock_metadata_rules.assert_called_once() + mock_metadata_rules.assert_called_once_with('rules') self.assertEqual(result, mock_metadata_rules.return_value) - @patch.object(clone_metadata_utils, 'create_metadata_items') - def test_create_metadata_item_field(self, mock_add_metadata_field): - """Test creating a single metadata field""" - mock_metadata_field = { - 'external_id': 'test_field', - 'type': 'string', - 'label': 'Test Field', - 'mandatory': False - } - - clone_metadata_utils.create_metadata_items('add_metadata_field', mock_metadata_field) - - mock_add_metadata_field.assert_called_once_with('add_metadata_field', mock_metadata_field) - - @patch.object(clone_metadata_utils, 'create_metadata_items') - def test_create_metadata_item_rule(self, mock_add_metadata_rule): - """Test creating a single metadata rule""" - mock_metadata_rule = { - 'external_id': 'test_rule', - 'condition': 'if', - 'metadata_field': { - 'external_id': 'test_field' - }, - 'results': [{ - 'value': 'test_value', - 'apply_to': ['metadata_field_external_id'] - }] - } - - clone_metadata_utils.create_metadata_items('add_metadata_rule', mock_metadata_rule) - - mock_add_metadata_rule.assert_called_once_with('add_metadata_rule', mock_metadata_rule) - - @patch.object(clone_metadata_utils, 'create_metadata_items') + @patch.object(clone_metadata_utils, 'sync_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_new_fields(self, mock_list, mock_create): - """Test comparing and creating new metadata fields""" - metadata_fields = { - 'metadata_fields': [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - }, - { - 'external_id': 'field2', - 'type': 'integer', - 'label': 'Field 2' - } - ] - } - - mock_source_fields = metadata_fields - mock_list.return_value = metadata_fields - mock_destination_fields = { - 'metadata_fields': [] - } - - with patch('builtins.input', return_value='y'): - clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) + @patch.object(clone_metadata_utils, 'compare_dicts') + @patch('cloudinary.config') + def test_clone_metadata_type_fields_success(self, mock_config, mock_compare, mock_list, mock_sync): + """Test _clone_metadata_type for fields""" + mock_config.return_value.cloud_name = 'source-cloud' + mock_list.return_value = [ + {'external_id': 'field1', 'type': 'string'} + ] + mock_compare.return_value = ( + {'field1': {'external_id': 'field1'}}, # source_map + ['field1'], # only_in_source + set() # common + ) + mock_sync.return_value = True - # Both fields should be created - self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][0], **self.mock_target_config) - mock_create.assert_any_call('add_metadata_field', mock_source_fields['metadata_fields'][1], **self.mock_target_config) + result = clone_metadata_utils._clone_metadata_type( + 'fields', 'external_id', self.mock_target_config, False + ) - result = clone_metadata_utils.list_metadata_items("metadata_fields", **self.mock_target_config) - mock_list.assert_called_once() - self.assertEqual(result, mock_list.return_value) + self.assertTrue(result) + mock_list.assert_any_call('fields') + mock_list.assert_any_call('fields', **self.mock_target_config) + mock_compare.assert_called_once() + mock_sync.assert_called_once() - @patch.object(clone_metadata_utils, 'create_metadata_items') + @patch.object(clone_metadata_utils, 'sync_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_new_rules(self, mock_list, mock_create): - """Test comparing and creating new metadata rules""" - metadata_rules = { - 'metadata_rules': [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - }, - { - 'external_id': 'rule2', - 'condition': 'if', - 'metadata_field': {'external_id': 'field2'}, - 'results': [{ - 'value': 'value2', - 'apply_to': ['target_field2'] - }] - } - ] - } - - mock_source_metadata_rules = metadata_rules - mock_list.return_value = metadata_rules - - mock_destination_metadata_rules = { - 'metadata_rules': [] - } - - with patch('builtins.input', return_value='y'): - clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) - # Both rules should be created - self.assertEqual(mock_create.call_count, 2) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][0], **self.mock_target_config) - mock_create.assert_any_call('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], **self.mock_target_config) - - result = clone_metadata_utils.list_metadata_items("metadata_rules", **self.mock_target_config) - mock_list.assert_called_once() - self.assertEqual(result, mock_list.return_value) - - @patch.object(clone_metadata_utils, 'create_metadata_items') - def test_compare_create_metadata_items_existing_fields(self, mock_create): - """Test comparing when fields already exist""" - mock_source_fields = { - 'metadata_fields': [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - } - ] - } - - # Simulate destination already having the field - mock_destination_fields = { - 'metadata_fields': [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - } - ] - } - - with patch('builtins.input', return_value='y'): - clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) - - # No fields should be created - mock_create.assert_not_called() + @patch.object(clone_metadata_utils, 'compare_dicts') + @patch('cloudinary.config') + def test_clone_metadata_type_rules_success(self, mock_config, mock_compare, mock_list, mock_sync): + """Test _clone_metadata_type for rules""" + mock_config.return_value.cloud_name = 'source-cloud' + mock_list.return_value = [ + {'name': 'rule1', 'condition': 'if'} + ] + mock_compare.return_value = ( + {'rule1': {'name': 'rule1'}}, + ['rule1'], + set() + ) + mock_sync.return_value = True - @patch.object(clone_metadata_utils, 'create_metadata_items') - def test_compare_create_metadata_items_existing_rules(self, mock_create): - """Test comparing when rules already exist""" + result = clone_metadata_utils._clone_metadata_type( + 'rules', 'name', self.mock_target_config, False + ) - mock_source_metadata_rules = { - 'metadata_rules': [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - } - ] - } - - # Simulate destination already having the rule - mock_destination_metadata_rules = { - 'metadata_rules': [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - } - ] - } + self.assertTrue(result) + mock_list.assert_any_call('rules') + mock_list.assert_any_call('rules', **self.mock_target_config) + mock_compare.assert_called_once() + mock_sync.assert_called_once() - with patch('builtins.input', return_value='y'): - clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) - - # No rules should be created - mock_create.assert_not_called() - - @patch.object(clone_metadata_utils, 'create_metadata_items') @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_mixed_scenario(self, mock_list, mock_create): - """Test comparing with mix of new and existing fields""" - metadata_fields = { - 'metadata_fields': [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - }, - { - 'external_id': 'field2', - 'type': 'integer', - 'label': 'Field 2' - } - ] - } - - # Simulate destination having only one field - mock_destination_fields = { - 'metadata_fields': [ - { - 'external_id': 'field1', - 'type': 'string', - 'label': 'Field 1' - } - ] - } + @patch('cloudinary.config') + def test_clone_metadata_fields_no_source_items(self, mock_config, mock_list): + """Test _clone_metadata_type for fields when source has no items""" + mock_config.return_value.cloud_name = 'source-cloud' + mock_list.return_value = [] + + result = clone_metadata_utils._clone_metadata_type( + 'fields', 'external_id', self.mock_target_config, False + ) - mock_source_fields= metadata_fields - mock_list.return_value = metadata_fields - - with patch('builtins.input', return_value='y'): - clone_metadata_utils.compare_create_metadata_items(mock_source_fields, mock_destination_fields, key="metadata_fields", **self.mock_target_config) - - # Only new_field should be created - mock_create.assert_called_once_with('add_metadata_field', mock_source_fields['metadata_fields'][1], **self.mock_target_config) - - result = clone_metadata_utils.list_metadata_items("metadata_fields", **self.mock_target_config) - mock_list.assert_called_once() - self.assertEqual(result, mock_list.return_value) - - @patch.object(clone_metadata_utils, 'create_metadata_items') - @patch.object(clone_metadata_utils, 'list_metadata_items') - def test_compare_create_metadata_items_mixed_rules_scenario(self, mock_list, mock_create): - """Test comparing with mix of new and existing rules""" - metadata_rules = { - 'metadata_rules': [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - }, - { - 'external_id': 'rule2', - 'condition': 'if', - 'metadata_field': {'external_id': 'field2'}, - 'results': [{ - 'value': 'value2', - 'apply_to': ['target_field2'] - }] - } - ] - } - - # Simulate destination having only one rule - mock_destination_metadata_rules = { - 'metadata_rules': [ - { - 'external_id': 'rule1', - 'condition': 'if', - 'metadata_field': {'external_id': 'field1'}, - 'results': [{ - 'value': 'value1', - 'apply_to': ['target_field1'] - }] - } - ] - } + self.assertFalse(result) + mock_list.assert_called_once_with('fields') - mock_source_metadata_rules = metadata_rules - mock_list.return_value = metadata_rules - - with patch('builtins.input', return_value='y'): - clone_metadata_utils.compare_create_metadata_items(mock_source_metadata_rules, mock_destination_metadata_rules, key="metadata_rules", **self.mock_target_config) - - # Only new_rule should be created - mock_create.assert_called_once_with('add_metadata_rule', mock_source_metadata_rules['metadata_rules'][1], **self.mock_target_config) + @patch.object(clone_metadata_utils, 'list_metadata_items') + @patch('cloudinary.config') + def test_clone_metadata_rules_no_source_items(self, mock_config, mock_list): + """Test _clone_metadata_type for rules when source has no items""" + mock_config.return_value.cloud_name = 'source-cloud' + mock_list.return_value = [] + + result = clone_metadata_utils._clone_metadata_type( + 'rules', 'name', self.mock_target_config, False + ) - result = clone_metadata_utils.list_metadata_items("metadata_rules", **self.mock_target_config) - mock_list.assert_called_once() - self.assertEqual(result, mock_list.return_value) + self.assertFalse(result) + mock_list.assert_called_once_with('rules') @patch.object(clone_module, 'handle_auto_pagination') @patch.object(clone_module, 'execute_single_request') @@ -373,7 +181,6 @@ def test_search_assets_default_expression(self, mock_search_class, mock_execute, mock_pagination.return_value = self.mock_search_result result = clone_module.search_assets(force=True, search_exp="") - # Verify default search expression is used mock_search.expression.assert_called_with("type:upload OR type:private OR type:authenticated") self.assertEqual(result, self.mock_search_result)