11import unittest
22from unittest .mock import patch , MagicMock
33import re
4+ import importlib .util
5+ import os
6+
7+ # Get the path to the clone module and load it directly to avoid conflicts with the command object
8+ clone_module_path = os .path .abspath (os .path .join (os .path .dirname (__file__ ), '..' , '..' , 'cloudinary_cli' , 'modules' , 'clone.py' ))
9+ spec = importlib .util .spec_from_file_location ("clone_module" , clone_module_path )
10+ clone_module = importlib .util .module_from_spec (spec )
11+ spec .loader .exec_module (clone_module )
412
5- from cloudinary_cli .modules .clone import search_assets , process_metadata
613from cloudinary_cli .defaults import logger
714
815
@@ -25,8 +32,8 @@ def setUp(self):
2532 ]
2633 }
2734
28- @patch ( 'cloudinary_cli.modules.clone. handle_auto_pagination' )
29- @patch ( 'cloudinary_cli.modules.clone. execute_single_request' )
35+ @patch . object ( clone_module , ' handle_auto_pagination' )
36+ @patch . object ( clone_module , ' execute_single_request' )
3037 @patch ('cloudinary.search.Search' )
3138 def test_search_assets_default_expression (self , mock_search_class , mock_execute , mock_pagination ):
3239 """Test search_assets with empty search expression uses default"""
@@ -35,14 +42,14 @@ def test_search_assets_default_expression(self, mock_search_class, mock_execute,
3542 mock_execute .return_value = self .mock_search_result
3643 mock_pagination .return_value = self .mock_search_result
3744
38- result = search_assets (force = True , search_exp = "" )
45+ result = clone_module . search_assets (force = True , search_exp = "" )
3946
4047 # Verify default search expression is used
4148 mock_search .expression .assert_called_with ("type:upload OR type:private OR type:authenticated" )
4249 self .assertEqual (result , self .mock_search_result )
4350
44- @patch ( 'cloudinary_cli.modules.clone. handle_auto_pagination' )
45- @patch ( 'cloudinary_cli.modules.clone. execute_single_request' )
51+ @patch . object ( clone_module , ' handle_auto_pagination' )
52+ @patch . object ( clone_module , ' execute_single_request' )
4653 @patch ('cloudinary.search.Search' )
4754 def test_search_assets_with_custom_expression (self , mock_search_class , mock_execute , mock_pagination ):
4855 """Test search_assets appends default types to custom expression"""
@@ -51,15 +58,15 @@ def test_search_assets_with_custom_expression(self, mock_search_class, mock_exec
5158 mock_execute .return_value = self .mock_search_result
5259 mock_pagination .return_value = self .mock_search_result
5360
54- result = search_assets (force = True , search_exp = "tags:test" )
61+ result = clone_module . search_assets (force = True , search_exp = "tags:test" )
5562
5663 # Verify custom expression gets default types appended
5764 expected_exp = "tags:test AND (type:upload OR type:private OR type:authenticated)"
5865 mock_search .expression .assert_called_with (expected_exp )
5966 self .assertEqual (result , self .mock_search_result )
6067
61- @patch ( 'cloudinary_cli.modules.clone. handle_auto_pagination' )
62- @patch ( 'cloudinary_cli.modules.clone. execute_single_request' )
68+ @patch . object ( clone_module , ' handle_auto_pagination' )
69+ @patch . object ( clone_module , ' execute_single_request' )
6370 @patch ('cloudinary.search.Search' )
6471 def test_search_assets_with_allowed_type (self , mock_search_class , mock_execute , mock_pagination ):
6572 """Test search_assets accepts allowed types"""
@@ -68,16 +75,16 @@ def test_search_assets_with_allowed_type(self, mock_search_class, mock_execute,
6875 mock_execute .return_value = self .mock_search_result
6976 mock_pagination .return_value = self .mock_search_result
7077
71- result = search_assets (force = True , search_exp = "type:upload" )
78+ result = clone_module . search_assets (force = True , search_exp = "type:upload" )
7279
7380 # Verify allowed type is accepted as-is
7481 mock_search .expression .assert_called_with ("type:upload" )
7582 self .assertEqual (result , self .mock_search_result )
7683
77- @patch ( 'cloudinary_cli.modules.clone. logger' )
84+ @patch . object ( clone_module , ' logger' )
7885 def test_search_assets_with_disallowed_type (self , mock_logger ):
7986 """Test search_assets rejects disallowed types"""
80- result = search_assets (force = True , search_exp = "type:facebook" )
87+ result = clone_module . search_assets (force = True , search_exp = "type:facebook" )
8188
8289 # Verify error is logged and False is returned
8390 mock_logger .error .assert_called_once ()
@@ -86,10 +93,10 @@ def test_search_assets_with_disallowed_type(self, mock_logger):
8693 self .assertIn ("facebook" , error_call )
8794 self .assertEqual (result , False )
8895
89- @patch ( 'cloudinary_cli.modules.clone. logger' )
96+ @patch . object ( clone_module , ' logger' )
9097 def test_search_assets_with_mixed_types (self , mock_logger ):
9198 """Test search_assets with mix of allowed and disallowed types"""
92- result = search_assets (force = True , search_exp = "type:upload OR type:facebook" )
99+ result = clone_module . search_assets (force = True , search_exp = "type:upload OR type:facebook" )
93100
94101 # Verify error is logged for disallowed type
95102 mock_logger .error .assert_called_once ()
@@ -129,7 +136,7 @@ def test_process_metadata_basic(self):
129136 'secure_url' : 'https://res.cloudinary.com/demo/image/upload/v1234567890/sample.jpg'
130137 }
131138
132- options , url = process_metadata (
139+ options , url = clone_module . process_metadata (
133140 res , overwrite = True , async_ = False , notification_url = None ,
134141 auth_token = None , ttl = 3600 , copy_fields = []
135142 )
@@ -153,7 +160,7 @@ def test_process_metadata_with_tags_and_context(self):
153160 'context' : {'key' : 'value' }
154161 }
155162
156- options , url = process_metadata (
163+ options , url = clone_module . process_metadata (
157164 res , overwrite = False , async_ = True , notification_url = 'http://webhook.com' ,
158165 auth_token = None , ttl = 3600 , copy_fields = ['tags' , 'context' ]
159166 )
@@ -175,7 +182,7 @@ def test_process_metadata_with_folder(self):
175182 'folder' : 'test_folder'
176183 }
177184
178- options , url = process_metadata (
185+ options , url = clone_module . process_metadata (
179186 res , overwrite = False , async_ = False , notification_url = None ,
180187 auth_token = None , ttl = 3600 , copy_fields = []
181188 )
@@ -193,7 +200,7 @@ def test_process_metadata_with_asset_folder(self):
193200 'asset_folder' : 'asset_folder_test'
194201 }
195202
196- options , url = process_metadata (
203+ options , url = clone_module . process_metadata (
197204 res , overwrite = False , async_ = False , notification_url = None ,
198205 auth_token = None , ttl = 3600 , copy_fields = []
199206 )
@@ -211,7 +218,7 @@ def test_process_metadata_with_display_name(self):
211218 'display_name' : 'Test Asset Display Name'
212219 }
213220
214- options , url = process_metadata (
221+ options , url = clone_module . process_metadata (
215222 res , overwrite = False , async_ = False , notification_url = None ,
216223 auth_token = None , ttl = 3600 , copy_fields = []
217224 )
@@ -234,7 +241,7 @@ def test_process_metadata_restricted_asset_no_auth_token(self, mock_private_url,
234241 'access_control' : [{'access_type' : 'token' }]
235242 }
236243
237- options , url = process_metadata (
244+ options , url = clone_module . process_metadata (
238245 res , overwrite = False , async_ = False , notification_url = None ,
239246 auth_token = None , ttl = 3600 , copy_fields = []
240247 )
@@ -259,7 +266,7 @@ def test_process_metadata_restricted_asset_with_auth_token(self, mock_cloudinary
259266 'access_control' : [{'access_type' : 'token' }]
260267 }
261268
262- options , url = process_metadata (
269+ options , url = clone_module . process_metadata (
263270 res , overwrite = False , async_ = False , notification_url = None ,
264271 auth_token = {'key' : 'value' }, ttl = 3600 , copy_fields = []
265272 )
@@ -290,7 +297,7 @@ def test_process_metadata_restricted_raw_asset_with_auth_token(self, mock_cloudi
290297 'access_control' : [{'access_type' : 'token' }]
291298 }
292299
293- options , url = process_metadata (
300+ options , url = clone_module . process_metadata (
294301 res , overwrite = False , async_ = False , notification_url = None ,
295302 auth_token = {'key' : 'value' }, ttl = 3600 , copy_fields = []
296303 )
0 commit comments